起因

在使用multiprocessing Pool 时,需要每个进程都对同一个字典进行操作。

爬坑

  1. 想到:from const import AIM_DICT 即多个进程共用同一个字典常量,但实际上如果用 id() 来检查不同进程中的字典会发现 id 并不相同,也就是每个进程在创建的过程中都会执行 import… 所以最后它们读写的字典并不是预想中的同一个。
  2. 考虑到创建多个进程的过程中, main 函数只会执行一次,想到在 main 中定义一个字典作为常量传给各个线程,使用 multiprocessing.Lock() 来避免资源同时访问。
    因为pool.map() 不能传入多个参数,所以用到了 functools.partial() 来固定不变的参数:锁和需要共享的字典。结果报错:
RuntimeError: Lock objects should only be shared between processes through inheritance

查阅资料发现报错因为多进程并不由同一父进程创建,所以 multiprocessing.Lock() 创建的锁不能传递,转而改用 multiprocessing.Manager().Lock()

  1. 使用 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相同。

  1. 查阅资料发现 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等。