起因
在使用multiprocessing Pool 时,需要每个进程都对同一个字典进行操作。
爬坑
- 想到:from const import AIM_DICT 即多个进程共用同一个字典常量,但实际上如果用 id() 来检查不同进程中的字典会发现 id 并不相同,也就是每个进程在创建的过程中都会执行 import… 所以最后它们读写的字典并不是预想中的同一个。
- 考虑到创建多个进程的过程中, main 函数只会执行一次,想到在 main 中定义一个字典作为常量传给各个线程,使用 multiprocessing.Lock() 来避免资源同时访问。
因为pool.map() 不能传入多个参数,所以用到了 functools.partial() 来固定不变的参数:锁和需要共享的字典。结果报错:
RuntimeError: Lock objects should only be shared between processes through inheritance
查阅资料发现报错因为多进程并不由同一父进程创建,所以 multiprocessing.Lock() 创建的锁不能传递,转而改用 multiprocessing.Manager().Lock()
- 使用 multiprocessing.Manager().Lock() 来锁定资源可以执行,但是共享的字典依然是初始状态,并未被更新。
import multiprocessing
import functools
def test(arg, aim_dict, lock):
print(id(aim_dict))
for i in aim_dict.keys():
lock.acquire()
aim_dict[i] += 1
lock.release()
print(arg)
if __name__ == '__main__':
pool = multiprocessing.Pool(processes=4)
manager = multiprocessing.Manager()
Aim_dict = dict([(i, 0) for i in range(4)])
LOCK = manager.Lock()
pt = functools.partial(test, aim_dict=Aim_dict, lock=LOCK)
pool.map(pt, list(range(100)))
pool.close()
pool.join()
print(Aim_dict)
print('id of Aim_dict is %s' % id(Aim_dict))
...已省略部分输出
1890629908880
89
2266441251360
95
1890629908880
90
98
2177221327248
96
1890629908880
99
97
{0: 0, 1: 0, 2: 0, 3: 0}
id of Aim_dict is 2255759450328
检查发现有一些进程传入的字典id相同,但都不和希望共享的字典id相同。
- 查阅资料发现 multiprocessing.Manager().dict() 创建的字典能用于共享,终于可以解决问题。
最终测试代码
import multiprocessing
import functools
def test(arg, aim_dict, lock):
# 注意要用 .keys() 不要直接写成 for i in aim_dict
for i in aim_dict.keys():
# 获取锁
lock.acquire()
aim_dict[i] += 1
# 完成字典操作,释放锁
lock.release()
if __name__ == '__main__':
# 创建线程池
pool = multiprocessing.Pool()
# 创建Manger对象用于管理进程间通信
manager = multiprocessing.Manager()
Aim_dict = manager.dict([(i, 0) for i in range(4)])
# 使用 Manager 生成锁
LOCK = manager.Lock()
pt = functools.partial(test, aim_dict=Aim_dict, lock=LOCK)
pool.map(pt, list(range(100)))
"""
使用 Pool.apply_async() 方法也能执行
for i in range(100):
pool.apply_async(test, (i, Aim_dict, LOCK))
"""
pool.close()
pool.join()
print(Aim_dict)
{0: 100, 1: 100, 2: 100, 3: 100}
不加资源锁时会出现错误:
def test(arg, aim_dict, lock):
'''不加锁时出现错误'''
for i in aim_dict.keys():
#lock.acquire()
aim_dict[i] += 1
#lock.release()
{0: 81, 1: 78, 2: 78, 3: 80}
总结
1、 本以为所有进程 test() 函数内部的字典和锁会是同样的id,即用同一个锁来控制相同的字典,但使用 id() 查看会发现各个进程内 id 值均不相同。所有 Manager 内部的实现机制还有待研究。
2、 对于共享的字典,一定要用 keys() 方法获取所有键, 否则可能多个进程同时访问字典而报错。 不建议在对字典遍历之前锁定资源,可能会造成死锁的情况,最好只在访问或修改共享资源的时候加锁,完成后立即解锁。
3、Lock 对象需要用 Manager.Lock() 生成 。
4、 Manager 还能生成其他需要共享的数据类型如 int 、list等。