今天,我们来介绍下Python的多进程编程和进程间通信的常用方式。

本篇代码基于Python3

多进程编程

由于Python的全局解释器锁(GIL)的存在,多线程在面对cpu密集型的任务时不能发挥多核cpu的性能。所以,在Python中,如果遇到cpu密集型的任务,通常会采用多进程的方式。多进程编程主要使用multiprocessing模块,使用方式和多线程差不多。

例如:

# coding: utf-8import multiprocessing
def run(total):
sum = 0
for i in range(total):
sum += i
print(sum)
def main():
process = multiprocessing.Process(target=run, args=(100000000,))
process.start()
process2 = multiprocessing.Process(target=run, args=(100000000,))
process2.start()
if __name__ == "__main__":
main()
也可以使用进程池:
# coding: utf-8import multiprocessing
def run(total):
sum = 0
for i in range(total):
sum += i
print(sum)
def main():
pool = multiprocessing.Pool(2)
# 使用进程池主要是在任务很多的时候避免频繁启动和关闭进程带来的资源消耗 args = [100000000, 100000000]
pool.map(run, args)
if __name__ == "__main__":
main()
在介绍多线程的线程池的时候,我们使用的是multiprocessing.dummy的Pool对象,虽然是在多进程的模块下,但是实际上却是多线程,而且和multiprocessing.Pool的使用上基本一致。所以,在我们需要在线程池和进程池之间来回切换时,直接替换掉导入的对象就可以了,代码基本不用改,使用起来很方便。
进程间通信
管道
# coding: utf-8from multiprocessing import Process, Pipe
def f(conn):
conn.send('hello')
print(conn.recv())
conn.close()
if __name__ == "__main__":
conn1, conn2 = Pipe()
p = Process(target=f, args=(conn2,))
p.start()
print(conn1.recv())
conn1.send('world')
p.join()
输出:
hello
world
其实,在非Windows平台上,Python中的管道是用socket实现的,在Pipe的源码可以看到。
队列
多进程的队列是在multiprocessing模块下Queue对象,主要有put和get两个方法。
# coding: utf-8import time
from multiprocessing import Process, Queue
import random
q = Queue()
def consumer(q):
while 1:
num = q.get()
if num is None:
break
result = num * 2
print(result)
if __name__ == '__main__':
c = Process(target=consumer, args=(q,))
c.start()
r = random.random()
print(r)
q.put(r)
q.put(None)
共享内存
主要通过multiprocessing.sharedctypes中的Value和Array来实现的。
multiprocessing.Value(typecode_or_type,*args,lock=True)
返回从共享内存分配的ctypes对象。typecode_or_type确定返回对象的类型:它是ctypes类型或array 模块使用的一种类型的typecode。 *args传递给该类型的构造函数。如果lock是True(默认),则会创建一个锁,以同步对该值的访问。lock也可以是Lock或RLock对象。如果lock是False,那么对返回对象的访问将不会被锁自动保护,因此它不是进程安全的。
multiprocessing.Array(typecode_or_type,size_or_initializer,*,lock = True)
返回从共享内存分配的ctypes数组。typecode_or_type确定返回数组的元素类型:它是ctypes类型或array模块使用的一种字符类型代码。如果size_or_initializer是一个整数,那么它确定数组的长度,并且该数组最初将归零。否则,size_or_initializer是一个用于初始化数组的序列,其长度决定了数组的长度。lock和上面Value的lock参数一样。
以下代码来自Python官方文档。
# coding: utf-8from multiprocessing import Process, Lock
from multiprocessing.sharedctypes import Value, Array
from ctypes import Structure, c_bool, c_double
lock = Lock()
class Point(Structure):
_fields_ = [('x', c_double), ('y', c_double)]
def modify(n, b, s, arr, A):
n.value **= 2
b.value = True
s.value = s.value.upper()
arr[0] = 10
for a in A:
a.x **= 2
a.y **= 2
if __name__ == "__main__":
n = Value('i', 7)
b = Value(c_bool, False, lock=False)
s = Array('c', b'hello world', lock=lock)
arr = Array('i', range(5), lock=True)
A = Array(Point, [(1.875, -6.25), (-5.75, 2.0)], lock=lock)
p = Process(target=modify, args=(n, b, s, arr, A))
p.start()
p.join()
print(n.value)
print(b.value)
print(s.value)
print(arr[:])
print([(a.x, a.y) for a in A])
输出:
49
True
b’HELLO WORLD’
[10, 1, 2, 3, 4]
[(3.515625, 39.0625), (33.0625, 4.0)]

多进程之间的通信还可以使用信号、socket来实现。信号可以发送给一个进程,不能传输大量的数据,而且信号在windows和linux的实现不同。socket的优势是可以实现跨机器通信。

此外,多进程中的Lock、RLock、Condition、Event、Semaphore和多线程的用法是一样的,这里就不多说了。

参考资料:https://docs.python.org/3/library/multiprocessing.html