你还在用单线程?Python的并发处理库的入门与使用详解!

 

concurrent提供了两种并发模型,一个是多线程ThreadPoolExecutor,一个是多进程ProcessPoolExecutor。对于IO密集型任务宜使用多线程模型。对于计算密集型任务应该使用多进程模型。

 

多线程模式适合IO密集型运算,这里我要使用sleep来模拟一下慢速的IO任务。同时为了方便编写命令行程序,这里使用Google fire开源库来简化命令行参数处理。

 

 

我们看到计算总共花费了大概5s,总共sleep了10s由两个线程分担,所以是5s。读者也许会问,为什么输出乱了,这是因为print操作不是原子的,它是两个连续的write操作合成的,第一个write输出内容,第二个write输出换行符,write操作本身是原子的,但是在多线程环境下,这两个write操作会交错执行,所以输出就不整齐了。如果将代码稍作修改,将print改成单个write操作,输出就整齐了(关于write是否绝对原子性还需要进一步深入讨论)

 

可以看到1s中就完成了所有的任务。这就是多线程的魅力,可以将多个IO操作并行化,减少整体处理时间。

多进程

 

相比多线程适合处理IO密集型任务,多进程适合计算密集型。接下来我们要模拟一下计算密集型任务。我的个人电脑有2个核心,正好可以体验多核心计算的优势。

那这个密集型计算任务怎么模拟呢,我们可以使用圆周率计算公式。

 

通过扩大级数的长度n,就可以无限逼近圆周率。当n特别大时,计算会比较缓慢,这时候CPU就会一直处于繁忙状态,这正是我们所期望的。

好,下面开写多进程并行计算代码

 

通过代码可以看出多进程模式在代码的编写上和多线程没有多大差异,仅仅是换了一个类名,其它都一摸一样。这也是concurrent库的魅力所在,将多线程和多进程模型抽象出了一样的使用接口。

 

看来耗时不能继续节约了,因为只有2个计算核心,2个进程已经足以榨干它们了,即使再多加进程也只有2个计算核心可用。

深入原理

concurrent用的时候非常简单,但是内部实现并不是很好理解。在深入分析内部的结构之前,我们需要先理解一下Future这个对象。在前面的例子中,executor提交(submit)任务后都会返回一个Future对象,它表示一个结果的坑,在任务刚刚提交时,这个坑是空的,一旦子线程运行任务结束,就会将运行的结果塞到这个坑里,主线程就可以通过Future对象获得这个结果。简单一点说,Future对象是主线程和子线程通信的媒介。

 

Future对象的内部逻辑简单一点可以使用下面的代码进行表示

 

 

 

 

 

我觉得作者的这张图还不够好懂,所以也单独画了一张图,请读者们仔细结合上面两张图,一起来过一边完整的任务处理过程。

  1. 主线程将任务塞进TaskQueue(普通内存队列),拿到Future对象
  2. 唯一的管理线程从TaskQueue获取任务,塞进CallQueue(分布式跨进程队列)
  3. 子进程从CallQueue中争抢任务进行处理
  4. 子进程将处理结果塞进ResultQueue(分布式跨进程队列)
  5. 管理线程从ResultQueue中获取结果,塞进Future对象
  6. 主线程从Future对象中拿到结果

 

跨进程队列

进程池模型中的跨进程队列是用multiprocessing.Queue实现的。那这个跨进程队列内部细节是怎样的,它又是用什么高科技来实现的呢

笔者仔细阅读了multiprocessing.Queue的源码发现,它使用无名套接字sockerpair来完成的跨进程通信,socketpair和socket的区别就在于socketpair不需要端口,不需要走网络协议栈,通过内核的套接字读写缓冲区直接进行跨进程通信。

 

当父进程要传递任务给子进程时,先使用pickle将任务对象进行序列化成字节数组,然后将字节数组通过socketpair的写描述符写入内核的buffer中。子进程接下来就可以从buffer中读取到字节数组,然后再使用pickle对字节数组进行反序列化来得到任务对象,这样总算可以执行任务了。同样子进程将结果传递给父进程走的也是一样的流程,只不过这里的socketpair是ResultQueue内部创建的无名套接字。

multiprocessing.Queue是支持双工通信,数据流向可以是父到子,也可以是子到父,只不过在concurrent的进程池实现中只用到了单工通信。CallQueue是从父到子,ResultQueue是从子到父。

总结

concurrent.futures框架非常好用,虽然内部实现机制异常复杂,读者也无需完全理解内部细节就可以直接使用了。但是需要特别注意的是不管是线程池还是进程池其内部的任务队列都是无界的,一定要避免消费者处理不及时内存持续攀升的情况发生。

进群:125240963  即可获取!

猜你喜欢

转载自www.cnblogs.com/PY2578/p/9173307.html