当我尝试使用多处理库在python中实现并行操作时,我发现某些进程不会以非直观的方式终止。
我的程序包括:
下面是一个简化的示例。 make_data
生成随机数并推送到队列,use_data
接收数据并计算平均值。总共生成2 * 1000 = 2000个数字,并全部使用。此代码按预期运行。毕竟,所有进程都终止了,队列中没有数据。
import random
from multiprocessing import Process, Queue
q = Queue(maxsize=10000)
def make_data(q):
for i in range(1000):
x = random.random()
q.put(x)
print("final line of make data")
def use_data(q):
i = 0
res = 0.0
while i < 2000:
if q.empty():
continue
i += 1
x = q.get()
res = res*(i-1)/i + x/i
print("iter %6d, avg = %.5f" % (i, res))
u = Process(target=use_data, args=(q,))
u.start()
p1 = Process(target=make_data, args=(q,))
p1.start()
p2 = Process(target=make_data, args=(q,))
p2.start()
u.join(timeout=10)
p1.join(timeout=10)
p2.join(timeout=10)
print(u.is_alive(), p1.is_alive(), p2.is_alive(), q.qsize())
结果:
final line of make data
final line of make data
iter 2000, avg = 0.49655
False False False 0
当我让制造商生成超出必要数据的数据时,事情发生了变化。
下面的代码与上面的代码的不同之处仅在于每个制造商生成5000个数据,因此并非所有数据都被使用。运行此命令时,它会打印最后一行的消息,但是制造商进程永远不会终止(需要Ctrl-C
停止)。
import random
from multiprocessing import Process, Queue
q = Queue(maxsize=10000)
def make_data(q):
for i in range(5000):
x = random.random()
q.put(x)
print("final line of make data")
def use_data(q):
i = 0
res = 0.0
while i < 2000:
if q.empty():
continue
i += 1
x = q.get()
res = res*(i-1)/i + x/i
print("iter %6d, avg = %.5f" % (i, res))
u = Process(target=use_data, args=(q,))
u.start()
p1 = Process(target=make_data, args=(q,))
p1.start()
p2 = Process(target=make_data, args=(q,))
p2.start()
u.join(timeout=10)
p1.join(timeout=10)
p2.join(timeout=10)
print(u.is_alive(), p1.is_alive(), p2.is_alive(), q.qsize())
结果:
final line of make data
final line of make data
iter 2000, avg = 0.49388
False True True 8000
# and never finish
在我看来,所有进程都运行到最后,所以想知道为什么它们仍然存在。有人可以帮助我了解这种现象吗?
我通过miniconda发行版在python 3.6.6
上运行了该程序。
答案 0 :(得分:2)
将项目放入队列的子进程被卡住,试图将对象实际放入队列。
正常的,非多重处理的Queue
对象完全在单个进程的地址空间中实现。在这种情况下,maxsize
是put()
调用块之前可以入队的项目数。但是,多处理Queue
对象是使用IPC机制实现的;通常是管道。并且OS管道可以将有限数量的字节排队(通常限制为8KB)。因此,当您的use_data()
进程在仅使2000个项目出队后终止时,make_data()
进程就会阻塞,因为在退出时将本地排队的项目刷新到IPC中时,它们的IPC通道已满。这意味着它们实际上并没有退出,因此join()
这些进程的尝试会无限期地阻塞。
实际上,您已经创建了一个死锁。发生的确切阈值取决于IPC通道可以缓冲多少数据。例如,在我的一台Linux服务器上,您的第二个示例在u.join()
和p1.join()
之间插入了该示例,可以可靠地工作:
for _ in range(4000):
q.get()
稍微减小该范围(例如降至3990)会产生间歇性的挂起。将该范围进一步减小(例如,减小到3500)将始终挂起,因为至少有一个进程将数据填充到队列块中,同时将其项刷新到IPC通道中。
这个故事的教训?在尝试等待进程终止之前,请始终完全耗尽多处理队列。