手册:multiprocessing — 基于进程的并行
Python multiprocessing模块中常用函数和类的统计
一. 进程通信
(对于进程间通信此处理解不清,可能有误)
进程间通信方式:
- 消息机制:Pipe、Queue
- 共享内存:Value、Array
- 共享文件:mmap模块
- 同步原语:Lock、RLock、Event、Semaphore、Condition。
消息机制 (Pipe、Queue)
- 效率高
- 帮我们处理好锁问题。
共享文件
- 1.效率低(共享数据基于文件,而文件是硬盘上的数据)
- 2.需要自己加锁处理
同步原语(进程锁, 信号量 , 事件 )
这些有锁的功能的东西(能够阻塞)是全局的,加锁可以保证多个进程修改同一块数据时,同一时间只能有一个任务可以进行修改,即串行的修改,没错,速度是慢了,但牺牲了速度却保证了数据安全。
多进程应该尽可能避免在进程间传递大量数据,越少越好。
二. Lock、RLock、Event、Semaphore、Condition、Pipe、Queue、JoinableQueue
- multiprocess.Value()
- multiprocess.Array()
- multiprocessing.Pipe()
- multiprocessing.Queue()
- multiprocessing.JoinableQueue()
- multiprocessing.Lock()
- multiprocessing.RLock()
- multiprocessing.Event()
- multiprocessing.Semaphore()
- multiprocessing.BoundedSemaphore()
- multiprocessing.Condition()
multiprocessing的Lock、RLock、Event、Semaphore、BoundedSemaphore、Condition接口与threading接口相同。
threading中的Lock、RLock、Event、Semaphore、BoundedSemaphore、Condition详解
multiprocessing.Queue()的接口与用于多线程的queue.Queue()类似。queue.Queue()比 multiprocessing.Queue()多两个方法task_done和join。multiprocessing.JoinableQueue()接口与queue.Queue()接口相同。
queue模块详解
使用场景:Pipe、Queue只适用于多个进程都是源于同一个父进程的情况。如果多个进程不是源于同一个父进程,只能用共享内存,信号量等方式,但是这些方式对于复杂的数据结构,例如Queue,dict,list等,使用起来比较麻烦,不够灵活,这种情况使用Manager。
使用方法:
- (multiprocessing的Value、Array、Lock、RLock、Event、Semaphore、BoundedSemaphore、Condition) + (multiprocessing.Process())
- (multiprocessing的Value、Array、Lock、RLock、Event、Semaphore、BoundedSemaphore、Condition) +(concurrent.futures.ProcessPoolExecutor())
1. Lock
1.1 Lock示例
import multiprocessing
def sub(a, lock):
print(f"sub_start;{a.value}")
# lock.acquire() # 对于该程序来说在此处获取锁效率更高,避免多次进程间通信
for i in range(100000):
lock.acquire()
b = a.value
b -= 1
a.value = b
lock.release()
# lock.release()
print(f"sub_end;{a.value}")
def add(a, lock):
print(f"add_start;{a.value}")
for i in range(100000):
lock.acquire()
b = a.value
b += 1
a.value = b
lock.release()
print(f"add_end;{a.value}")
if __name__ == '__main__':
a = multiprocessing.Value("i", 0) # a 属于共享内存中的变量
lock = multiprocessing.Lock() # lock也属于共享内存中的变量
tasks = [multiprocessing.Process(target=add, args=(a, lock), name="add"), multiprocessing.Process(target=sub, args=(a, lock), name="sub")]
[task.start() for task in tasks]
[task.join() for task in tasks]
print(a.value)
"""
运行结果:
add_start;0
sub_start;5011
add_end;3511
sub_end;0
0
Process finished with exit code 0
"""
1.2 补充:multiprocessing.Value()
1.2.1 Value的构造函数:
Value的初始化非常简单,直接类似Value(‘d’, 0.0)即可,具体构造方法为:
multiprocessing.Value(typecode_or_type, *args[, lock])。
该方法返回从共享内存中分配的一个ctypes 对象,其中typecode_or_type定义了返回的类型。它要么是一个ctypes类型,要么是一个代表ctypes类型的code。比如c_bool和’b’是同样的,因为’b’是c_bool的code。
ctypes是Python的一个外部函数库,它提供了和C语言兼任的数据类型,可以调用DLLs或者共享库的函数,能被用作在python中包裹这些库。
*args是传递给ctypes的构造参数
1.2.2 Value的使用
对于共享整数或者单个字符,初始化比较简单,参照下图映射关系即可。
注意,如果我们使用的code在上表不存在,则会抛出:
size = ctypes.sizeof(type_)
TypeError: this type has no size
如果共享的是字符串,则在上表是找不到映射关系的,就是没有code可用。所以我们需要使用原始的ctype类型
ctype类型可从下表查阅
1.2.3 示例
from multiprocessing import Value
from ctypes import *
a = Value("i", 2)
print(a.value)
print(a)
a = Value(c_int, 2)
print(a.value)
print(a)
a = Value("f", 3.14)
print(a.value)
print(a)
a = Value(c_float, 3.14)
print(a.value)
print(a)
a = Value('c', b'a')
print(a.value.decode("ascii"))
print(a)
a = Value(c_char, b'a')
print(a.value.decode("ascii"))
print(a)
a = Value(c_char_p, bytes("中国", "utf-8"))
print(a.value.decode("utf-8"))
print(a)
"""
运行结果:
2
<Synchronized wrapper for c_long(2)>
2
<Synchronized wrapper for c_long(2)>
3.140000104904175
<Synchronized wrapper for c_float(3.140000104904175)>
3.140000104904175
<Synchronized wrapper for c_float(3.140000104904175)>
a
<Synchronized wrapper for c_char(b'a')>
a
<Synchronized wrapper for c_char(b'a')>
中国
<Synchronized wrapper for c_char_p(2736578502120)>
Process finished with exit code 0
"""
2. RLock
import multiprocessing
def fibonacci(num, total, lock):
if num == 2 or num == 1:
return 1
else:
lock.acquire()
print("lock已为total上锁")
total.value = fibonacci(num - 1, total, lock) + fibonacci(num - 2, total, lock)
lock.release()
print("lock已为total释放锁")
return total.value
if __name__ == '__main__':
total = multiprocessing.Value("i", 0)
lock = multiprocessing.RLock()
task = multiprocessing.Process(target=fibonacci, args=(6, total, lock))
task.start()
task.join()
print(total.value)
"""
运行结果:
lock已为total上锁
lock已为total上锁
lock已为total上锁
lock已为total上锁
lock已为total释放锁
lock已为total释放锁
lock已为total上锁
lock已为total释放锁
lock已为total释放锁
lock已为total上锁
lock已为total上锁
lock已为total释放锁
lock已为total释放锁
lock已为total释放锁
8
Process finished with exit code 0
"""
3. Event
import multiprocessing
import time
def test0(event):
event.clear()
print(f"{multiprocessing.current_process().name}_start========")
time.sleep(1)
print(f"{multiprocessing.current_process().name}_end========")
event.set()
def test1(event):
event.wait()
print(f"{multiprocessing.current_process().name}_end========")
if __name__ == '__main__':
event = multiprocessing.Event()
task0 = multiprocessing.Process(target=test0, args=(event,), name="Pro_test0")
task1 = multiprocessing.Process(target=test1, args=(event,), name="Pro_test0")
task0.start()
task1.start()
task0.join()
task1.join()
"""
运行结果:
Pro_test0_start========
Pro_test0_end========
Pro_test0_end========
Process finished with exit code 0
"""
4. Semaphore
import multiprocessing
import time
import random
def db_connect(num, semaphore):
print(f"thread =={num}== is preparing to connect to db.")
semaphore.acquire()
time.sleep(int(random.random()*10)/10) # 模拟连接db的过程
print(f"connection =={num}== already complete.")
semaphore.release()
if __name__ == '__main__':
semaphore = multiprocessing.Semaphore(value=2)
tasks = [multiprocessing.Process(target=db_connect, args=(num, semaphore)) for num in range(7)]
[task.start() for task in tasks]
[task.join() for task in tasks]
semaphore.release() # Semaphore对象释放多次不会抛出异常
"""
运行结果:
thread ==0== is preparing to connect to db.
thread ==1== is preparing to connect to db.
connection ==0== already complete.
thread ==2== is preparing to connect to db.
connection ==1== already complete.
connection ==2== already complete.
thread ==4== is preparing to connect to db.
thread ==3== is preparing to connect to db.
connection ==3== already complete.
thread ==5== is preparing to connect to db.
thread ==6== is preparing to connect to db.
connection ==4== already complete.
connection ==6== already complete.
connection ==5== already complete.
Process finished with exit code 0
"""
5. Condition
import multiprocessing
import time
def task_1(condition_obj):
proc_name = multiprocessing.current_process().name
print('开始 %s' % proc_name)
with condition_obj:
print('%s运行结束,开始运行task_2' % proc_name)
condition_obj.notify_all()
def task_2(condition_obj):
proc_name = multiprocessing.current_process().name
print('开始 %s' % proc_name)
with condition_obj:
condition_obj.wait()
print('task_2 %s 运行结束' % proc_name)
if __name__ == '__main__':
condition_obj = multiprocessing.Condition()
s1 = multiprocessing.Process(name='s1', target=task_1, args=(condition_obj,))
s2_clients = [multiprocessing.Process(name='task_2[{}]'.format(i), target=task_2, args=(condition_obj,),) for i in range(1, 3)]
for c in s2_clients:
c.start()
time.sleep(1)
s1.start()
s1.join()
for c in s2_clients:
c.join()
"""
运行结果:
开始 task_2[1]
开始 task_2[2]
开始 s1
s1运行结束,开始运行task_2
task_2 task_2[2] 运行结束
task_2 task_2[1] 运行结束
Process finished with exit code 0
"""
6. 消息机制Pipe
Pipe()函数返回两个对象 conn1 和 conn2 ,这两个对象表示管道的两端。
Pipe()函数有一个可选参数 duplex,参数 duplex 的默认值为 True,表示该管道是双向的,即两个对象都可以发送和接收消息。如果把参数 duplex 设置为 False ,表示该管道是单向的,即 conn1 只能用于接收消息,conn2 只能用于发送消息。
# 双向管道
import multiprocessing
def test0(conn):
conn.send("test1,我是test0")
print(conn.recv())
def test1(conn):
print(conn.recv())
conn.send("test0,你好")
if __name__ == '__main__':
conn0, conn1 = multiprocessing.Pipe(duplex=True)
task0 = multiprocessing.Process(target=test0, args=(conn0,))
task1 = multiprocessing.Process(target=test1, args=(conn1,))
task0.start()
task1.start()
task0.join()
task1.join()
"""
运行结果:
test1,我是test0
test0,你好
Process finished with exit code 0
"""
# 单向管道
import multiprocessing
def test0(conn):
# conn.send("test1,我是test0")
print(conn.recv())
def test1(conn):
# print(conn.recv())
conn.send("test0,你好")
if __name__ == '__main__':
conn0, conn1 = multiprocessing.Pipe(duplex=False) # conn0接收消息,conn1发送消息
task0 = multiprocessing.Process(target=test0, args=(conn0,))
task1 = multiprocessing.Process(target=test1, args=(conn1,))
task0.start()
task1.start()
task0.join()
task1.join()
"""
运行结果:
test0,你好
Process finished with exit code 0
"""
7. 消息机制Queue
底层也是通过Pipe实现。
原型:Queue([maxsize])
参数介绍
- maxsize是队列中允许最大项数,省略则无大小限制。
当一个队列为空的时候如果再用get取则会堵塞,所以取队列的时候一般是用到get_nowait()方法,这种方法在向一个空队列取值的时候会抛一个Empty异常,所以更常用的方法是先判断一个队列是否为空,如果不为空则取值。
队列Queue的实例对象queue中常用的方法
queue.qsize()
返回队列的大小queue.empty()
如果队列为空,返回True,反之Falsequeue.full()
如果队列满了,返回True,反之Falsequeue.get([block[, timeout]])
获取队列,timeout等待时间queue.get_nowait()
相当Queue.get(False) ,非阻塞 Queue.put(item) 写入队列,timeout等待时间queue.put_nowait(item)
相当Queue.put(item, False)
import multiprocessing
import time
def test0(queue, num):
for i in range(num*3):
queue.put(f"{num}: {i}")
def test1(queue):
flag = 0
while True:
if not queue.empty():
print(queue.get())
flag = 0
else:
time.sleep(0.1)
flag += 1
if flag == 2:
break
if __name__ == '__main__':
queue = multiprocessing.Queue()
task0 = [multiprocessing.Process(target=test0, args=(queue, num)) for num in range(1, 3)]
task1 = multiprocessing.Process(target=test1, args=(queue,))
[task.start() for task in task0]
task1.start()
[task.join() for task in task0]
task1.join()
"""
运行结果:
1: 0
1: 1
1: 2
1: 3
1: 4
2: 0
2: 1
2: 2
2: 3
2: 4
2: 5
2: 6
2: 7
2: 8
2: 9
Process finished with exit code 0
"""
8. 消息机制JoinableQueue
原型:JoinableQueue([maxsize])
- 这就像是一个Queue对象,但队列允许项目的使用者通知生成者项目已经被成功处理。通知进程是使用共享的信号和条件变量来实现的。
参数介绍
- maxsize是队列中允许最大项数,省略则无大小限制。
当一个队列为空的时候如果再用get取则会堵塞,所以取队列的时候一般是用到get_nowait()方法,这种方法在向一个空队列取值的时候会抛一个Empty异常,所以更常用的方法是先判断一个队列是否为空,如果不为空则取值。
JoinableQueue的实例queue队列中常用的方法:
queue.qsize()
返回队列的大小queue.empty()
如果队列为空,返回True,反之Falsequeue.full()
如果队列满了,返回True,反之Falsequeue.get([block[, timeout]])
获取队列,timeout等待时间queue.get_nowait()
相当Queue.get(False) ,非阻塞 Queue.put(item) 写入队列,timeout等待时间queue.put_nowait(item)
相当Queue.put(item, False)- JoinableQueue的实例queue除了与Queue对象相同的方法之外还具有:
queue.task_done()
:使用者使用此方法发出信号,表示queue.get()的返回项目已经被处理。如果调用此方法的次数大于从队列中删除项目的数量,将引发ValueError异常queue.join()
:生产者调用此方法进行阻塞,直到队列中所有的项目均被处理。阻塞将持续到队列中的每个项目均调用queue.task_done()方法为止
import multiprocessing
import time
def test0(queue, num):
for i in range(num*3):
queue.put(f"{num}: {i}")
def test1(queue):
flag = 0
while True:
if not queue.empty():
print(queue.get())
queue.task_done()
flag = 0
else:
time.sleep(0.1)
flag += 1
if flag == 2:
break
if __name__ == '__main__':
queue = multiprocessing.JoinableQueue(maxsize=1)
task0 = [multiprocessing.Process(target=test0, args=(queue, num)) for num in range(1, 3)]
task1 = multiprocessing.Process(target=test1, args=(queue,))
[task.start() for task in task0]
task1.start()
# [task.join() for task in task0]
# task1.join()
queue.join()
"""
运行结果:
1: 0
1: 1
2: 0
1: 2
2: 1
2: 2
2: 3
2: 4
2: 5
Process finished with exit code 0
"""
三. Pool、Manager
1. 进程池Pool
Pool类可以提供指定数量的进程供用户调用,当有新的请求提交到Pool中时,如果池还没有满,就会创建一个新的进程来执行请求。如果池满,请求就会告知先等待,直到池中有进程结束,才会创建新的进程来执行这些请求
下面介绍一下multiprocessing 模块下的Pool类下的几个方法:
apply(func[, args=()[, kwds={}]])
- 该函数用于传递不定参数,主进程会被阻塞直到函数执行结束
apply_async(func[, args=()[, kwds={}[, callback=None]]])
- 与apply用法一致,但它是非阻塞的且支持结果返回后进行回调
map(func, iterable[, chunksize=None])
- Pool类中的map方法,与内置的map函数用法行为基本一致,它会使进程阻塞直到结果返回
注意:虽然第二个参数是一个迭代器,但在实际使用中,必须在整个队列都就绪后,程序才会运行子进程
map_async(func, iterable[, chunksize[, callback]])
- 与map用法一致,但是它是非阻塞的
close()
- 关闭进程池(pool),使其不再接受新的任务
terminal()
- 结束工作进程,不再处理未处理的任务
join()
- 主进程阻塞等待子进程的退出, join方法要在close或terminate之后使用
2. 服务器进程管理器对象moprocessing.Manager()
2.1 概念
- Manager() 返回的管理器对象控制一个
服务器进程
,该进程保存Python对象并允许其他进程使用代理操作它们。 - 服务器进程管理器比使用共享内存对象更
灵活
,因为它支持Python支持的所有数据类型。 - 单个管理器可以通过网络由
不同计算机上
的进程共享。但是,它们比使用共享内存慢
。 - 源于不同父进程的子进程之间可以通过Manager()对象通信。
原理:先启动一个ManagerServer进程,这个进程是阻塞
的,它监听一个socket
,然后其他进程(ManagerClient)通过socket来连接到ManagerServer,实现通信。
2.2 Manager() 返回的管理器支持类型
- Manager().dict()
- Manager().Array()
- Manager().Namespace()
- Manager().Value()
- Manager().list()
- Manager().Queue()
- Manager().Lock()
- Manager().RLock()
- Manager().Event()
- Manager().Semaphore()
- Manager().BoundedSemaphore()
- Manager().Condition()
Manager()的Lock、RLock、Event、Semaphore、BoundedSemaphore、Condition接口与threading接口相同。
threading中的Lock、RLock、Event、Semaphore、BoundedSemaphore、Condition详解
Manager()的Queue、Value、Array的接口与multiprocess的Queue、Value、Array相同。
因此此处只谈接口list、dict、Namespace接口。
使用场景:
- 源于不同父进程的子进程之间可以通过Manager()对象通信。
- 不同主机间的进程通信。
- 同一父进程创建的子进程间通信,不建议使用,效率较低。
使用方法:
- Manager() + Pool()
- Manager() + multiprocessing.Process(),Manager() + concurrent.futures.ProcessPoolExecutor(),不建议使用,因Manager()底层走的是socket,效率较低。
2.3 示例
2.3.1 list、dict
# list
import multiprocessing
def add(temp_list, lock):
print(f"++++add_{multiprocessing.current_process().ident}_start:{temp_list[0]}++++")
for i in range(10000):
lock.acquire()
temp_list[0] += 1
lock.release()
print(f"++++add_{multiprocessing.current_process().ident}_end:{temp_list[0]}++++")
def sub(temp_list, lock):
print(f"++++sub_{multiprocessing.current_process().ident}_start:{temp_list[0]}++++")
for i in range(10000):
lock.acquire()
temp_list[0] -= 1
lock.release()
print(f"++++sub_{multiprocessing.current_process().ident}_end:{temp_list[0]}++++")
if __name__ == '__main__':
manager = multiprocessing.Manager()
temp_list = manager.list([0])
lock = manager.Lock()
pool = multiprocessing.Pool(2)
for num in range(2):
pool.apply_async(add, args=(temp_list, lock))
for num in range(2):
pool.apply_async(sub, args=(temp_list, lock))
pool.close()
pool.join()
print(temp_list)
"""
运行结果:
++++add_14680_start:0++++
++++add_9740_start:20++++
++++add_14680_end:19983++++
++++sub_14680_start:19991++++
++++add_9740_end:19992++++
++++sub_9740_start:19982++++
++++sub_14680_end:15++++
++++sub_9740_end:0++++
[0]
Process finished with exit code 0
"""
# dict
import multiprocessing
def add(temp_dict, lock):
print(f"++++add_{multiprocessing.current_process().ident}_start:{temp_dict['test']}++++")
for i in range(10000):
lock.acquire()
temp_dict['test'] += 1
lock.release()
print(f"++++add_{multiprocessing.current_process().ident}_end:{temp_dict['test']}++++")
def sub(temp_dict, lock):
print(f"++++sub_{multiprocessing.current_process().ident}_start:{temp_dict['test']}++++")
for i in range(10000):
lock.acquire()
temp_dict['test'] -= 1
lock.release()
print(f"++++sub_{multiprocessing.current_process().ident}_end:{temp_dict['test']}++++")
if __name__ == '__main__':
manager = multiprocessing.Manager()
temp_dict = manager.dict({'test': 0})
lock = manager.Lock()
pool = multiprocessing.Pool(2)
for num in range(2):
pool.apply_async(add, args=(temp_dict, lock))
for num in range(2):
pool.apply_async(sub, args=(temp_dict, lock))
pool.close()
pool.join()
print(temp_dict['test'])
"""
运行结果:
++++add_11360_start:0++++
++++add_476_start:17++++
++++add_476_end:19990++++
++++sub_476_start:19998++++
++++add_11360_end:19999++++
++++sub_11360_start:19991++++
++++sub_476_end:13++++
++++sub_11360_end:0++++
0
Process finished with exit code 0
"""
补充list、dict
Manager处理list、dict等可变数据类型时,需要注意一个陷阱,即Manager对象无法监测到它引用的可变对象值的修改,需要通过触发__setitem__方法来让它获得通知。
而触发__setitem__方法比较直接的办法就是增加一个中间变量,如同在C语言中交换两个变量的值一样:
int a=1;int b=2;int tmp=a;a=b;b=tmp;
示例:
# Manager对象无法监测到它引用的可变对象值的修改
import multiprocessing
def test(idx, test_dict, lock):
lock.acquire()
test_dict['test'][idx] = idx
lock.release()
if __name__ == '__main__':
manager = multiprocessing.Manager()
temp_dict = manager.dict()
lock = manager.Lock()
temp_dict['test'] = {}
pool = multiprocessing.Pool(4)
for i in range(10):
pool.apply_async(test, args=(i, temp_dict, lock))
pool.close()
pool.join()
print(temp_dict)
"""
运行结果:
{'test': {}}
Process finished with exit code 0
"""
# 通过触发__setitem__方法来让Manager对象获得通知
import multiprocessing
def test(idx, test_dict, lock):
lock.acquire()
row = test_dict['test']
row[idx] = idx
test_dict['test'] =row
lock.release()
if __name__ == '__main__':
manager = multiprocessing.Manager()
temp_dict = manager.dict()
lock = manager.Lock()
temp_dict['test'] = {}
pool = multiprocessing.Pool(4)
for i in range(10):
pool.apply_async(test, args=(i, temp_dict, lock))
pool.close()
pool.join()
print(temp_dict)
"""
运行结果:
{'test': {0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 6: 6, 7: 7, 5: 5, 8: 8, 9: 9}}
Process finished with exit code 0
"""
2.3.2 Namespace
Namespace对象没有公共的方法,但是有可写的属性。Namespace支持python中所有的基本数据类型,但与dict、list相同Manager对象无法监测到它引用的可变对象值的修改,需要通过触发__setitem__方法来让它获得通知。
当使用manager返回的namespace的proxy的时候,_属性值属于proxy,跟原来的namespace没有关系。
import multiprocessing
def f(ns):
ns.x *= 10
ns.y *= 10
# ns.l[0] *= 10 # Manager对象无法监测到它引用的可变对象值的修改,需要通过触发__setitem__方法来让它获得通知。
l = ns.l
l[0] *= 10
ns.l = l
if 'testkey' in ns.d:
v = ns.d['testkey'] * 10
ns.d = {'testkey': v}
if __name__ == '__main__':
manager = multiprocessing.Manager()
ns = manager.Namespace()
ns.x = 1
ns.y = 2
ns._z = 5 # this is an attribute of the proxy
ns.d = {'testkey': 3}
ns.l = [4]
print('before', ns)
p = multiprocessing.Process(target=f, args=(ns,))
p.start()
p.join()
print('after', ns)
"""
运行结果:
before Namespace(d={'testkey': 3}, l=[4], x=1, y=2)
after Namespace(d={'testkey': 30}, l=[40], x=10, y=20)
Process finished with exit code 0
"""
[参考博客]
Python进程池multiprocessing.Pool的用法
python 多进程共享全局变量之Manager()详解
python 关于multiprocessing中在Namespace的实例下保存dict/list的疑问