我想要实现的:类似下载任务管理程序,最多同时下载5个,当下载任务有20个时,只同时下载5个,等待正在下载的5个任务完成一个,再开始下一个新的下载任务。
起因:我有一个python程序,运行时当鼠标点击视频,就下载这个视频。我用subprocess来执行下载程序,从而实现了多进程同时下载。
但是这样有个问题,当我频繁的点击视频,前面的下载任务还没完成,后面的下载任务就被激活了,进程里会同时执行非常多的下载程序,这导致了电脑非常的卡顿,并且很多下载程序因此终止。这并不优雅。
所以我希望python程序最多运行n个(如5个)下载程序,如果已经运行5个下载程序了,我后续的点击视频的事件不会马上激活下载程序,而是等待前5个下载程序完成其中一个,然后再开始,以此类推,后续的点击都会排队等待前5个下载视频完成其中1个再开始下载。
我想到的是queue,queue只有5格长,下载完成了出列,然后在排队等待的任务加入队列,但我完全不知道怎么去模拟“排队等待”这个效果,而且我觉得queue也不完全符合我的想法,因为queue必须是早进的早出,但可能晚入列的比早入列的下载完。
在百度上搜索queue和subprocess关键字没能找到答案,国内关于subprocess的帖子似乎很少。于是我到google搜索了一下,找到了一个完全符合我需求的帖子。
链接:Stackflow:Python multiple subprocess with a pool/queue recover output as soon as one finishes and launch next job in queue?
这个帖子的提问大概是:我用python的subprocess创建了多进程,想要结合pool(池)或者queue(队列)来实现:当一个任务完成时,下一个任务加入queue(队列)并开始执行。
得到的最高投票的回答如下:
ThreadPool could be a good fit for your problem, you set the number of worker threads and add jobs, and the threads will work their way through all the tasks.
ThreadPool可以很好的符合你问题的需求,你可以设置工作线程的最大数量并添加任务,这些线程将以自己的方式完成所有任务。
该回答附带的源码如下
from multiprocessing.pool import ThreadPool
import subprocess
def work(sample):
my_tool_subprocess = subprocess.Popen('mytool {}'.format(sample),shell=True, stdout=subprocess.PIPE)
line = True
while line:
myline = my_tool_subprocess.stdout.readline()
#here I parse stdout..
num = None # set to the number of workers you want (it defaults to the cpu count of your machine)
tp = ThreadPool(num)
for i in all_samples:
tp.apply_async(work, (sample,))
tp.close()
tp.join()
ThreadPool,线程池。
虽叫“线程”,但和多进程还是多线程无关,我用subprocess,就是多进程,ThreadPool就是一个任务管理员罢了,我只需要关注这些任务,和最大同时执行的任务数,剩下的,交给他就完事了~
然后我改变成了我的代码
# 伪代码
def startDownload(url):
subprocess.Popen("start python download.py "+url, shell=True)
threadManager = ThreadPool(5)
While 点击视频:
url = 获取点击的视频的链接()
threadManager.apply_async(startDownload, url)
接下来我做了一个控制变量的实验。
实验条件,threadManager = ThreadPool(3),发起5个任务
多进程发起方式 | 是否有start | shell是否true | download.py发起方式 | download.py是否wait() | thread有无async | thread有无close和join | 结果resultOftheExperiment |
---|---|---|---|---|---|---|---|
os.system | 无 | * | subprocess.Popen | wait() | 有 | 有 | 实现目的,但输出混乱,在前三个进程未执行完时按下关闭按钮,整个程序没被关掉,前三个任务似乎被关闭,开始第四个任务和第五个任务,再按一次关闭按钮才将程序关闭 |
os.system | start | * | subprocess.Popen | wait() | 有 | 有 | 5个进程以以新窗口的方式打开,同时进行,关闭主进程,子进程不会被关闭,似乎已经“独立” |
subprocess.Popen | 无 | false | subprocess.Popen | wait() | 有 | 有 | 5个进程同时运行,输出混乱,按下关闭按钮直接关闭整个程序 |
subprocess.Popen | 无 | true | subprocess.Popen | wait() | 有 | 有 | 5个进程同时运行,输出混乱,按下关闭按钮直接关闭整个程序 |
subprocess.Popen | start | false | subprocess.Popen | wait() | 有 | 有 | 根本无法发起下载任务,无报错,没有反应 |
subprocess.Popen | start | true | subprocess.Popen | wait() | 有 | 有 | 5个进程以以新窗口的方式打开,同时进行,关闭主进程,子进程不会被关闭,似乎已经“独立” |
看来要采用os.system的方式发起进程,并且不能加上start来开启新的窗口。
然后我进行了新一轮实验
多进程发起方式 | 是否有start | shell是否true | download.py发起方式 | download.py是否wait() | thread有无async | thread有无close和join | 结果resultOftheExperiment |
---|---|---|---|---|---|---|---|
os.system | 无 | * | subprocess.Popen | wait() | 有 | 有 | 实现目的,但输出混乱,在前三个进程未执行完时按下关闭按钮,整个程序没被关掉,前三个任务似乎被关闭,开始第四个任务和第五个任务,再按一次关闭按钮才将程序关闭 |
os.system | 无 | * | subprocess.Popen | wait() | 无 | 有 | 只启动一个下载任务,并且监听点击视频事件被阻塞,但实际上仍在监听,在这个下载任务结束后,自动开起下一个下载任务 |
os.system | 无 | * | subprocess.Popen | wait() | 有 | 无 | 实现目的,和【1】的反应一样。因为我的点击视频事件监听函数不会自己结束,所以我觉得不需要close和join来阻塞主进程结束 |
看来必须使用apply_async,如果主进程自己不会关闭,则不需要threadManager.close()和threadManager.join()来让主进程等待子进程完成任务。
然后我进行了新一轮实验
多进程发起方式 | shell是否true | download.py发起方式 | 是否start | download.py是否wait() | 结果resultOftheExperiment |
---|---|---|---|---|---|
os.system | * | subprocess.Popen | 无 | wait() | 实现目的,但输出混乱,在前三个进程未执行完时按下关闭按钮,整个程序没被关掉,前三个任务似乎被关闭,开始第四个任务和第五个任务,再按一次关闭按钮才将程序关闭 |
os.system | * | subprocess.Popen | 无 | 无 | 同时执行5个下载任务,输出混乱 |
os.system | * | subprocess.Popen | start | wait() | 5个进程以以新窗口的方式打开,同时进行,关闭主进程,子进程不会被关闭,似乎已经“独立” |
os.system | * | subprocess.Popen | start | 无 | 5个进程以以新窗口的方式打开,同时进行,关闭主进程,子进程不会被关闭,似乎已经“独立” |
os.system | * | os.system | 无 | * | 实现目的,但输出混乱,在前三个进程未执行完时按下关闭按钮,整个程序没被关掉,前三个任务似乎被关闭,开始第四个任务和第五个任务,再按一次关闭按钮才将程序关闭 |
os.system | * | os.system | start | * | 5个进程以以新窗口的方式打开,同时进行,关闭主进程,子进程不会被关闭,似乎已经“独立” |
如果用subprocess.popen,则必须加wait阻塞,如果用os.system,则天然可以阻塞。都不能加start,加start,start成功程序即代表结束。
总结:用apply_async和os.system,并且不加start即可满足需求。
# 伪代码
def startDownload(url):
os.system("start python download.py "+url)
threadManager = ThreadPool(5)
While 点击视频:
url = 获取点击的视频的链接()
threadManager.apply_async(startDownload, url)
如果主进程不用os.system而是subprocess,并且wait会发生什么呢
然后我做了如下实验
多进程发起方式 | 是否有start | shell是否true | 结果resultOftheExperiment |
---|---|---|---|
os.system | 无 | * | 实现目的,但输出混乱,在前三个进程未执行完时按下关闭按钮,整个程序没被关掉,前三个任务似乎被关闭,开始第四个任务和第五个任务,再按一次关闭按钮才将程序关闭 |
subprocess.Popen+wait | 无 | 有 | 实现目的,但输出混乱,在前三个进程未执行完时按下关闭按钮,整个程序没被关掉,前三个任务似乎被关闭,开始第四个任务和第五个任务,再按一次关闭按钮才将程序关闭 |
看来加了个wait,让函数等待subprocess完成,也可以实现阻塞效果。所以我估计subprocess不加wait,“创建进程”这个工作一做完,ThreadPool就认为这个函数执行结束了,所以价格wait也能实现效果。
但是目前输出还是混乱的,之前为什么要研究start,是因为start可以实现打开新窗口并执行python指令,但是前面的实验也可以看到,加了start后这些子进程都独立了,脱离了主进程,因为关闭主进程,子进程仍在执行,所以可以推断start其实本身就是调用了系统的start,然后启动下载程序,而不是由主进程直接唤起下载程序。
我们也可以尝试subprocess+ wait阻塞+start的组合,同样发现子进程独立,5个任务同时下载的情况。
所以我们迫切需要找一种既能打开新窗口运行下载程序,又能让下载程序隶属于ThreadPool被管理。
在google上找到了这篇文章
Stackflow:subprocess.Popen in different console
最多投票给出的代码是:
from subprocess import Popen, CREATE_NEW_CONSOLE
Popen('cmd', creationflags=CREATE_NEW_CONSOLE)
input('Enter to exit from Python script...')
我加了个wait改造成我的代码
# 伪代码
from subprocess import Popen, CREATE_NEW_CONSOLE
from multiprocessing.pool import ThreadPool
def startDownload(url):
command = "start python download.py "+url
p = Popen(command, creationflags=CREATE_NEW_CONSOLE)
p.wait()
threadManager = ThreadPool(5)
While 点击视频:
url = 获取点击的视频的链接()
threadManager.apply_async(startDownload, url)
而download.py用的是os.system或者subprocess+wait
这个代码完美的满足的我的需求。
下载任务管理,分窗口输出进度。
优雅,永不过时
所以总结一下
本次涉及到的因素有
os.system()
poepn+CREATE_NEW_CONSOLE+wait
subprocess.popen+wait
command是否含有start
首先os.system是阻塞的
subprocess.popen+wait是阻塞的
但只要这两者都加上“start”,就不阻塞了,因为他们执行start,相当于是“叫系统去开启”,而不是自己开启,他们叫完,他们事情就没了。这也是为什么start能开启新窗口。