前言
前段时间,我和我的领导回到了母校,和我的师父师母聚餐。聚餐点了很多东西,大碗宽面,牛肉炒饭,韩国烤肉,吃都吃不完。虽然我的领导最近长得比以前p了些,但是吃饭速度还是慢悠悠。唉,要是我的领导能有个三头六臂,每个手都夹菜,每个头都去啃,那吃饭速度可就蹭蹭地涨上去了啊!
人无法三头六臂,但在Python里,我们可以做到。
并发&并行实验
要想实现三头六臂的效率,不走单一顺序流,我们不仅需要让多个任务能够并发(Concurrent),还能够并行(Parallel)运作。
假使吃饭吃到一半,人有三急,摘花回来继续用膳,那么如果把“吃饭”与“解手”当作两个任务,那么它们便是便是并发运作,但不并行。如果太追求效率,蹲坑恰饭,那便即是并发,也是并行了。
在Python中,我们可以用三种方式实现并发。但是并不是所有的方法,都支持并行。
这三种方法是:
- 多线程 Multi Threading
- 多进程 Multi Processing
- 异步IO Async IO
其中,异步IO的实现方法,我们在第四章《玩转豆瓣二百五:下》中已经介绍过。多线程和多进程的实现方法,可以参考Python官方文档,或者干脆直接看下面的实例。
通过一个简单的运行时间测试我们就可以发现,这三种方法中,哪些方法是能够真正利用并行的效率:
1 | from multiprocessing import Pool |
结果出炉!
1 | Sequential: 18.116519 |
我们发现,只有采用多进程的方式,能够符合我们的并行猜测。其它方式,甚至比顺序执行还要慢一些,应当不是并行运行。
为什么是多进程?
线程与进程
首先,我们简要介绍一下线程与进程。
- 线程(Thread):操作系统调度最小单位,顺序执行的程序流
- 进程(Process):计算机程序的实例,线程的容器
进程,好比是一个特定工作的流程,是相对宏观的;线程,则是一个个子任务的流程,是相对微观的。一个进程中,可以容纳多个不同的线程以执行不同类型的工作。同一个进程的不同线程之间,共享了许多当前进程信息,相互独立性较弱;而同一个操作系统的进程之间,共享信息较少,相互独立性强。
在实际执行程序的时候,对于单个(核)CPU而言,同一时刻只能跑一个特定的线程。CPU通过不停地切换不同线程实现各个线程任务的并发,并且由于CPU手速太快,造成了我们一按ctrl + alt + del
所看到的,同一时刻几十个进程都在同时跑的假象。但是,对于多核CPU而言,我们就可以做到在多个CPU上,并发并且并行多个线程,增加执行效率。但即便如此,Python的多线程,却无法做到这一点。
全局解释器锁GIL
Python多线程无法利用多核CPU的优势,其罪魁祸首,在于全局解释器锁(GIL,Global Interpreter Lock)
了解GIL,可以看这个资料:UnderstandingGIL
运行Python代码,需要通过解释器(Intepreter)进行。Python解释器在读取了一行Python代码后,就会将其执行,执行完后,再读取下一行代码,以此类推。
GIL能够使得同一时刻,只有一个线程能在执行。Python的官方实现Cython就采用了这种机制,保证Python进程中资源的状态能够同步(synchronize)到各个线程中。可以看到,GIL简单粗暴,让我们省去了资源同步的担忧,但相对地,造成了一种同一时刻一个线程包场的景象,不能满足细粒度的资源同步操作。
因此,解决这个问题的办法就是开启多个进程,让每一个进程都有自己的解释器跟GIL。这样,就能实现多任务的并行了。
多进程资源共享——TornadoCenter
进程之间的资源信息通常是不共享的,因此要借由系统自身的机制。比如说内存信息共享,在Python的多进程模块multiprocessing
中,就提供了Pipe(管道)
、Queue(队列)
等方式,使得不同进程之间的数据可以共享。
在我的TornadoCenter小项目中,就有这样的一个实例——主进程是cmd小黑框命令行程序,通过规定的指令可以开启服务器进程与客户端进程。客户端进程通过网络读写往服务器进程发送数据,而服务器进程则通过Queue
队列发送数据给主进程。Tornado是一个著名的Python网络框架,因此能满足这方面的需求。
主进程大概设计如下:
1 | def __init__(self): |
我们来拆解一下业务:
- 初始化变量
- cmd_state: 根据指令查找对应执行函数的reference
- server_holder: 管理服务器进程
- client_holder: 管理客户端进程
- 函数
- loop:主循环,使得我们在cmd界面上能一直输入指令,输错了则显示帮助
- dispatch: 根据指令与cmd_state,查找&执行对应函数
对于管理服务器进程的server_holder
,我们需要让其持有服务器进程的实例process
,互通服务器进程数据的通道queue
,以及查询服务器进程状态的一些接口。
1 | class TornadoTCPServerHolder(BaseCSHolder): |
物以类聚,我们可以将服务器进程的管理抽象到一个类中。这个类继承了另外一个叫BaseCSHolder
的类,具备打印日志以及编辑参数的功能。
1 | class BaseCSHolder(Logger): |
总的来看,在启动服务器进程之后,同时也起一个线程执行monitor_loop
的逻辑,监控Queue
里的数据。每当Queue
里有数据的时候,就取出来数据,打印出来。
对于服务器进程的实例(Server Process Instance),我们可以让其持有服务器实例(Server Instance)、主线程传递的信息(比如服务器参数与Queue
)以及其它自身信息。服务器进程实例相当于是服务器实例的容器。
1 | class TornadoTCPServerProcess(Process, Logger): |
而最后,对于服务器实例,就是实实在在地继承服务器框架了。
Tornado框架的用法,可以参考官方文档
1 | class TornadoTCPServer(TCPServer, Logger): |
可以看到,每次服务器实例在收到数据后,就会把数据解码放到Queue
队列头里。这样在主线程那一端,就可以实时在队列尾取出数据打印了。
这样,我们就实现了多进程之间的资源共享。怎么样,要不要试试“千手观音”~
总结
有了多进程与异步的支持,Python在基础任务调度上,更是增强了一个层次
Easy Python第六章——多进程并行,也标志着Easy Python系列进入尾声
第七章,会对Easy Python系列做最终的整理
See ya~