Python 为什么这个小片段使用maxtasksperchild、numpy.random.randint和numpy.random.seed的多处理挂起?
我有一个python脚本,它以随机方式并发处理numpy数组和图像。为了在生成的进程中具有适当的随机性,我将一个随机种子从主进程传递给工作进程,以便它们进行播种 当我对Python 为什么这个小片段使用maxtasksperchild、numpy.random.randint和numpy.random.seed的多处理挂起?,python,numpy,multiprocessing,python-multiprocessing,numpy-random,Python,Numpy,Multiprocessing,Python Multiprocessing,Numpy Random,我有一个python脚本,它以随机方式并发处理numpy数组和图像。为了在生成的进程中具有适当的随机性,我将一个随机种子从主进程传递给工作进程,以便它们进行播种 当我对池使用maxstasksparchild时,我的脚本在运行Pool.map多次后挂起 以下是重现问题的最小片段: #此代码在多次处理后停止。池工作线程将被一次性替换。 #由于MAXSTAKSPERCHILD参数为Pool,它们被替换 来自多处理导入池 将numpy作为np导入 def工作人员(n): #删除np.random.se
池使用maxstasksparchild
时,我的脚本在运行Pool.map
多次后挂起
以下是重现问题的最小片段:
#此代码在多次处理后停止。池工作线程将被一次性替换。
#由于MAXSTAKSPERCHILD参数为Pool,它们被替换
来自多处理导入池
将numpy作为np导入
def工作人员(n):
#删除np.random.seed解决了这个问题
np.random.seed(1)#任何种子值
返回1234#平凡的返回值
#删除maxtasksperchild可以解决此问题
ppool=Pool(20,maxtasksperchild=5)
i=0
尽管如此:
i+=1
#删除np.random.randint(10)或将其从循环中取出可以解决这个问题
rand=np.random.randint(10)
l=[3]#对ppool.map的简单输入
结果=ppool.map(工人,l)
打印i,结果[0]
这是输出
1 1234
2 1234
3 1234
.
.
.
99 1234
100 1234 # at this point workers should've reached maxtasksperchild tasks
101 1234
102 1234
103 1234
104 1234
105 1234
106 1234
107 1234
108 1234
109 1234
110 1234
1 1234
2 1234
3 1234
.
.
.
99 1234
100 1234#此时,工人应已到达maxtasksperchild任务
101 1234
102 1234
103 1234
104 1234
105 1234
106 1234
107 1234
108 1234
109 1234
110 1234
然后无限期地挂起
我可能会用python的random
替换numpy.random
,然后解决这个问题。但是,在我的实际应用程序中,worker将执行我无法控制的用户代码(作为参数提供给worker),并允许在该用户代码中使用numpy.random
函数。因此,我有意为全局随机生成器(独立地为每个进程)设置种子
这是用Python 2.7.10、numpy 1.11.0、1.12.0和1.13.0、Ubuntu和OSX测试的,使用numpy.random.seed
不是线程安全的numpy.random.seed
全局更改种子的值,而据我所知,您正在尝试在本地更改种子
看
如果确实要实现的是在每个工作进程开始时对生成器进行种子设定,则以下是一个解决方案:
def worker(n):
# Removing np.random.seed solves the problem
randgen = np.random.RandomState(45678) # RandomState, not seed!
# ...Do something with randgen...
return 1234 # trivial return value
这是一个完整的答案,因为它不适合评论
在玩了一会儿之后,这里的一些东西闻起来像一个小虫子。我能够复制冻结错误,此外还有一些不应该发生的奇怪事情,比如手动播种发电机不工作
def rand_seed(rand, i):
print(i)
np.random.seed(i)
print(i)
print(rand())
def test1():
with multiprocessing.Pool() as pool:
[pool.apply_async(rand_seed, (np.random.random_sample, i)).get()
for i in range(5)]
test1()
有输出
0
0
0.3205032737431185
1
1
0.3205032737431185
2
2
0.3205032737431185
3
3
0.3205032737431185
4
4
0.3205032737431185
0
0
0.5488135039273248
1
1
0.417022004702574
2
2
0.43599490214200376
3
3
0.5507979025745755
4
4
0.9670298390136767
另一方面,不将np.random.random_sample作为参数传递也可以
def rand_seed2(i):
print(i)
np.random.seed(i)
print(i)
print(np.random.random_sample())
def test2():
with multiprocessing.Pool() as pool:
[pool.apply_async(rand_seed, (i,)).get()
for i in range(5)]
test2()
有输出
0
0
0.3205032737431185
1
1
0.3205032737431185
2
2
0.3205032737431185
3
3
0.3205032737431185
4
4
0.3205032737431185
0
0
0.5488135039273248
1
1
0.417022004702574
2
2
0.43599490214200376
3
3
0.5507979025745755
4
4
0.9670298390136767
这表明一些严重的愚蠢行为正在幕后进行。不过,我不知道还有什么好说的
基本上,它看起来像numpy.random.seed不仅修改了“seed state”变量,还修改了random\u sample
函数本身。事实证明,这是由线程化.Lock
和多处理
的Python错误交互造成的
np.random.seed
和大多数np.random.
函数使用threading.Lock
来确保线程安全。一个np.random.*
函数生成一个随机数,然后更新种子(跨线程共享),这就是需要锁的原因。请参阅和(由np.random.random()和其他人使用)
现在,这是如何在上面的代码片段中引起问题的
简而言之,代码段挂起是因为分叉时继承了threading.Lock
状态。因此,当一个子代在父代中获得锁的同时被分叉时(通过np.random.randint(10)
),子代会死锁(在np.random.seed
)
@njsmith在本期github中对此进行了解释
多处理。池生成一个后台线程来管理工作线程:
它在后台循环调用_维护_池:
如果工作进程退出,例如由于maxtasksperchild限制,则_维护_池调用_重新填充_池:
然后_repopulate_pool分叉了一些新的工人,仍然在这个后台线程中:
因此,发生的情况是,最终你会变得不走运,在主线程调用某个np.random函数并持有锁的同时,多处理决定派生一个子线程,该子线程从已经持有np.random锁开始,但持有它的线程不见了。然后,孩子尝试调用np.random,这需要锁,所以孩子会死锁
这里的简单解决方法是不将fork用于多处理。如果您使用spawn或forkserver启动方法,那么这应该会消失
为了妥善解决。。。。啊。我想我们。。需要注册pthread_atfork pre fork处理程序,该处理程序在fork之前获取np.random锁,然后将其释放?实际上,我想我们需要对numpy中的每个锁都这样做,这需要像保持每个随机状态对象的弱集这样的东西,而且_FFTCache似乎也有一个锁
(从好的方面来看,这也给了我们一个机会,可以重新初始化子对象中的全局随机状态,在用户没有明确设置种子的情况下,我们确实应该这样做。)
谢谢但是,np.random.RandomState(1)
不会为随机生成器设置种子。另外,你能告诉我在文档中它在哪里谈到了numpy.random.seed
的线程安全,我找不到它如果按线程安全你的意思是每个进程将生成相同的随机数…是的,这就是为什么我将一个随机种子从主进程传递到每个工作区的原因,这取决于你想要实现什么。如果每个工人都有一个新的发电机,种子是45678,那么这是可行的。据我所知,randgen
现在是