上一篇去爬壁纸的文章用的是每下载都使用一个新的线程去下,这种方式比较简单,但用起来好像不是很高效,而且进度显示也是很混乱,于是就想着使用线程池去管理下载任务,并使用更加好看的进度条显示进度。
先上一个效果图:
线程池
python我没找到有自带的线程池实现(类似于Java那种),但自己实现一个简单的其实也不难。线程池的说得简单点就是一堆线程集合,不过线程是可复用的,避免了重复开启线程的开销。
线程池主要包含几个部分
- 线程管理* 工作线程
- 任务队列
线程管理
基于此,首先得创建线程管理器。线程管理我把它定义成是一个创建工作线程以及分配任务到具体一个工作线程的过程,先上完整代码:
1 | __DEFAULT_NUM__=5 |
Worker是具体的工作线程定义(见下),在WorkPool初始化时就将所有的工作线程都创建出来,比较简单粗暴,但也够用;add_task是对外添加任务用的,里面实现有一个调度过程,为了各个工作线程工作平均,selectWorker会根据当前各个工作线程的任务数据来决定当前新任务将被交给哪个工作线程去完成。
1 | add_task(self,func,*args) |
add_task接受一个函数指针和这个函数的参数作用参数,args是一个tuple,类似于Java中的
1 | add_task(Object...args) |
但tuple是不能修改的。
选择好哪个工作线程后,就将函数与参数都放到去工作线程对应的工作队列中。
wait函数是用于判断结束的,当所有的工作线程都退出时,调用方应该知道。
工作线程
工作线程是算是核心,也是先上代码:
1 | class Worker(threading.Thread): |
在构造函数里先创建一个工作队列(Queue),并为每一个工作线程分派一个id,每一个工作线程本质上都是一个Thread。
add_task接受函数与参数,并将其丢到工作队列中,并将func与args打包成一个tuple统一丢到工作队列中就好了。
当首次接收到工作任务时,工作线程开始工作 ,run()函数就开始执行了。因为工作线程需要不断从工作队列中获取任务并执行,所以需要一个循环。
1 | self.work_queue.get(block=False) |
这一句从工作队列中获取任务,并不阻塞,当获取不到时就会抛出异常,由于任务是开始时一次过添加进去的,当获取不到任务时抛异常退出后就会认为工作线程工作结束了。
当从工作线程中获取到函数与参数后,就可以直接调用执行了
1 | func(*args) |
进度条
由于在终端显示进度条比较麻烦,上一篇里使用的要sys.stdout.write并且不换行来达到刷新的进度的效果,但毕竟还是不太好看。这次我使用了progressive来实现。
从progressive提供的example里可以看出,它是使用字典和arDescriptor来创建一个或多个进度条的。
看示例:
1 | test_d = { |
最终出来的效果是
其使用的是leaf_values表示的是对应进度条的进度,是一个Value对象,当修改此值是就会反映到进度条上,不过需要不断地刷新。
1 | # Create blessings.Terminal instance |
创建一个ProgressTree用将字典数据构建出具有层级关系的进度条。但如果需要将进度条画在终端上或者进行刷新,则需要调用
1 | # After the cursor position is first saved (in the first draw call) |
理解了progressTree的使用方法后,就可以将其应用到线程池上了。
1 | class TaskGraphics: |
结合上面的注释,大概也能看出运行原理了。另外,由于刷新进度需要不断地循环,所以不能放在主线程中,需要另开线程去刷新,否则其他工作就没法做了。
带进度条的线程池
结合上面两节,完整代码如下:
1 | #!/usr/bin/python |
需要讲几个点
1 | …… |
由于具体的进度需要在具体的任务中才能获取,比方说下载进度,下载进度的计算需要由具体任务去算,作用回调到工作线程中去更新进度条,但因为任务加进线程池时是不可能知道线程池里的回调函数的,所以在真正调用任务函数时,需要添加一个新的回调函数,由于参数本来是tuple,所以需要重新创建一个新的tuple作用参数传给任务函数,注意args_callback=(self.update,)里括号的逗号不能少,否则连接出来的结果不对。当然,具体的任务函数的参数里也要预留一个这样的回调,如
1 | def download(self,dirName,fileName,url,rateHook=None): |
rateHook就是回调的函数指针。
另外,由于progressive里的字典参数需要变换名字时,作为字典的key不能直接删除,且由于进度条是使用key来作用进度条的名字的,我需要动态地修改这个,但也不能使用index来定位具体是哪个,所以需要一个 indicator={} 来定义key是在什么位置,以便之后需要换key名字的时候可以直接根据id来定位。另外,由于这是一个字典,里面是按key来排序的,我不能因为换了key后就把进度条的顺序给乱了,所以需要给每个进度条都安排一个统一前缀的key,如“task_1,task_2”。
最后,再结合上一篇的爬壁纸操作,就能实现文章开头的效果了,task的名字是正在下载的文件名:
结合updateTask 和 updateValue 就可以实现进度条的更新和下载名的更新了。
依赖
No module named progressive.bar
pip install progressiveNo module named blessings
pip install blessings