Python subprocess ThreadPool 优雅的任务管理解决方案记录

我想要实现的:类似下载任务管理程序,最多同时下载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能开启新窗口。

猜你喜欢

转载自blog.csdn.net/weixin_45518621/article/details/126584946