4

我有一个 python 脚本,它以随机方式同时处理 numpy 数组和图像。为了在生成的进程中有适当的随机性,我将一个随机种子从主进程传递给工作人员,以便他们被播种。

当我使用maxtasksperchildfor 时Pool,我的脚本在运行后挂起Pool.map多次后挂起。

以下是重现问题的最小片段:

# This code stops after multiprocessing.Pool workers are replaced one single time.
# They are replaced due to maxtasksperchild parameter to Pool
from multiprocessing import Pool
import numpy as np

def worker(n):
    # Removing np.random.seed solves the issue
    np.random.seed(1) #any seed value
    return 1234 # trivial return value

# Removing maxtasksperchild solves the issue
ppool = Pool(20 , maxtasksperchild=5)
i=0
while True:
    i += 1
    # Removing np.random.randint(10) or taking it out of the loop solves the issue
    rand = np.random.randint(10)
    l  = [3] # trivial input to ppool.map
    result = ppool.map(worker, l)
    print i,result[0]

这是输出

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

然后无限期挂起。

我可能会numpy.random用 python替换random并摆脱这个问题。然而,在我的实际应用程序中,工作人员将执行我无法控制的用户代码(作为工作人员的参数),并希望允许numpy.random在该用户代码中使用函数。所以我有意为全局随机生成器播种(每个进程独立)。

这是用 Python 2.7.10、numpy 1.11.0、1.12.0 和 1.13.0、Ubuntu 和 OSX 测试的

4

3 回答 3

4

事实证明,这是来自 和 的 Python 错误threading.Lock交互multiprocessing

np.random.seed并且大多数np.random.*函数使用 athreading.Lock来确保线程安全。一个np.random.*函数生成一个随机数,然后更新种子(跨线程共享),这就是需要锁的原因。请参阅np.random.seedcont0_array(由np.random.random()和其他人使用)。

现在这如何导致上述代码段出现问题?

简而言之,代码片段挂起是因为threading.Lock分叉时继承了状态。因此,当一个子节点同时被分叉时,父节点 (by np.random.randint(10)) 中获得了锁,子节点死锁 (at np.random.seed)。

@njsmith 在这个 github 问题中解释它https://github.com/numpy/numpy/issues/9248#issuecomment-308054786

multiprocessing.Pool 产生一个后台线程来管理工作人员:https ://github.com/python/cpython/blob/aefa7ebf0ff0f73feee7ab24f4cdcb2014d83ee5/Lib/multiprocessing/pool.py#L170-L173

它在后台循环调用_maintain_pool:https ://github.com/python/cpython/blob/aefa7ebf0ff0f73feee7ab24f4cdcb2014d83ee5/Lib/multiprocessing/pool.py#L366

如果工作人员退出,例如由于 maxtasksperchild 限制,则 _maintain_pool 调用 _repopulate_pool:https ://github.com/python/cpython/blob/aefa7ebf0ff0f73feee7ab24f4cdcb2014d83ee5/Lib/multiprocessing/pool.py#L240

然后 _repopulate_pool 派生了一些新的工作人员,仍然在这个后台线程中:https ://github.com/python/cpython/blob/aefa7ebf0ff0f73feee7ab24f4cdcb2014d83ee5/Lib/multiprocessing/pool.py#L224

所以发生的事情是最终你会倒霉,同时你的主线程正在调用一些 np.random 函数并持有锁,多处理决定派生一个孩子,这从 np.random 已经持有的锁开始,但是持有它的线程已经消失了。然后孩子尝试调用 np.random,这需要获取锁,因此孩子死锁。

这里的简单解决方法是不要将 fork 与多处理一起使用。如果您使用 spawn 或 forkserver 启动方法,那么这应该会消失。

为了适当的修复.... ughhh。我想我们..需要注册一个 pthread_atfork pre-fork 处理程序,该处理程序在 fork 之前获取 np.random 锁,然后在之后释放它?真的,我想我们需要为 numpy 中的每个锁执行此操作,这需要保留每个 RandomState 对象的弱集,并且 _FFTCache 似乎也有一个锁......

(从好的方面来说,这也让我们有机会重新初始化孩子的全局随机状态,在用户没有明确播种的情况下,我们真的应该这样做。)

于 2017-06-13T11:22:50.473 回答
1

使用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                                                                         
于 2017-06-11T21:54:44.263 回答
0

使它成为一个完整的答案,因为它不适合评论。

玩了一会儿,这里的东西闻起来像一个 numpy.random 错误。我能够重现冻结错误,此外还有其他一些不应该发生的奇怪事情,例如手动播种生成器不起作用。

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

另一方面,不将 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.5488135039273248
1
1
0.417022004702574
2
2
0.43599490214200376
3
3
0.5507979025745755
4
4
0.9670298390136767

这表明幕后正在发生一些严重的愚蠢行为。虽然不知道还能说什么......

基本上看起来 numpy.random.seed 不仅修改了“种子状态”变量,还修改了random_sample函数本身。

于 2017-06-12T23:14:16.080 回答