每一个pythoner对GIL是又爱又恨,GIL给我们提供极大便利的同时,也带来了难以突破的性能瓶颈。我们知道,因为GIL的存在,使得一个python实例只能运行在一个Core上,那有没有办法开启多个GIL,让它们分别跑在不同的Core上,用以提升性能呢?答案是完全可行的,并且最新的python版本已经集成了相应的工具。
1. multiprocessing包 早在2008年,python社区就已经推出了专为多进程程序设计的multiprocessing包 [PEP 371] ,这个包模仿了threading包的大多数接口,大大降低了多进程程序的开发难度,同时也使得以往使用了threading包的程序仅需做极少量的修改即可享受多进程带来的性能提升。如官方给出的一则example :from threading import Thread as worker def afunc(number): print(number * 3) t = worker(target=afunc, args=(4,)) t.start() t.join()
将这个多线程版本的程序修改为多进程版本,仅需修改第一行的import语句即可:
from processing import process as worker def afunc(number): print(number * 3) t = worker(target=afunc, args=(4,)) t.start() t.join()
当然对于大型的进程体,我们更乐意把任务和数据都封装到一个类中,multiprocessing包同样提供了这样的实现:
class WorkProc(multiprocessing.Process): def run(self): ... proc = WorkProc(args) proc.start() ... proc.join()
2. 似乎一切都很美好同时,multiprocessing包提供了基础的进程同步工具,如 Pipe , Queue , Mutex , Semaphore 等。然而,这些工具就目前来说,也只能说能用,够用,但是离完美还有不少距离。举两个简单例子。
2.1 例一:Semaphore不能初始化为一个负值信号量class threading.Semaphore(value=1)
The optional argument gives the initial value for the internal counter; it defaults to 1. If the value given is less than 0, ValueError is raised.
在Java的世界里(当然在某些系统中也有类似的实现),Semaphore可以被初始化为一个负值信号量,这意味着我们可以让某个进程(或线程)先等待资源,而此资源由另外的进程(或线程)产生。一个比较常见的场景如下:
我们程序中需要初始化大量数据 我们想把这些初始化操作分派到多个进程中 当全部进程初始化完成后,主进程继续往下执行当我们使用负值信号量时,这个过程是相当自然的。然而,当没有了负值信号量时,我们只能用其他的办法,让工作进程通知主进程。
一个可行的办法是,让主进程持有针对每一个工作进程独立的信号量(或管道),当全部信号量(或管道)都收到了工作进程的消息后,主进程继续往下执行。明显这是一个妥协,一个很脏的办法。
同类问题还有 Semaphore不能一次acquire多个资源 。
2.2 例二:繁琐低效的进程间共享内存当进程间需要大量的数据通信时,Pipe、Queue和Socket等往往因速度太慢而被嫌弃,进程间共享内存才是王道。
linux里有及其方便的 <shm.h> ,直接通过指针共享内存,相当方便。而Java里也有极其高效的 nio包 。然而更真实的情况是,一般的程序开发很少用到进程间共享内存,因为无论是Linux还是Java,都有完整的线程支持,很多时候线程即可较好的承担我们的需求,一般无需进行进程开发。而在python的世界里,事情变得奇怪。
一般的数据共享可以使用multiprocessing自带的 Value 和 Array ,然而Value和Array都只能使用 multiprocessing.sharedctypes 中为数不多的几个数据类型,即使进一步封装的 multiprocessing.Manager 也只提供了为数不多的数据类型,无法满足复杂数据类型的进程间交互。详见 multiprocessing ― Process-based parallelism 。
尤其是目前python很多的软件包都使用C/C++编写核心处理模块,这些第三方软件包的数据类型更是千奇百怪,使得进程间通信变得更复杂。
就以目前广泛使用的包 numpy 为例。numpy的核心完全由C/C++编写,其使用最广泛的数据类型是 ndarray ,与C/C++的数据较为相似。若我想将一个二维的ndarray对象用于进程间通信,我需要编写以下代码:
tmp = r.reshape(n*m) r = multiprocessing.Array('d', tmp, lock=False or True) # 这个数据类型'd'只能是`multiprocessing.sharedctypes`中定义的数据类型 r = numpy.ctypeslib.as_array(r) r = r.reshape((n, m))
这段代码的大致意思就是:
将二维ndarray对象转换成一维对象tmp 这个一维对象转换为一个ctypes对象,这个ctypes对象才是真正共享的对象 通过numpy的一个函数将这个ctypes对象映射为numpy的一个一维的ndarray对象 最后将这个ndarray对象二维化。现在这个ndarray对象才是真正的进程间共享对象%