Quantcast
Channel: CodeSection,代码区,Python开发技术文章_教程 - CodeSec
Viewing all articles
Browse latest Browse all 9596

Parallel in Python

$
0
0
0. GIL

每一个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对象才是真正的进程间共享对象

%


Viewing all articles
Browse latest Browse all 9596

Trending Articles