为什么 ProcessPoolExecutor 和 Pool 会因 super() 调用而崩溃?
Why do ProcessPoolExecutor and Pool crash with a super() call?
1.为什么以下使用 concurrent.futures
模块的 Python 代码永远挂起?
import concurrent.futures
class A:
def f(self):
print("called")
class B(A):
def f(self):
executor = concurrent.futures.ProcessPoolExecutor(max_workers=2)
executor.submit(super().f)
if __name__ == "__main__":
B().f()
该调用引发了一个不可见的异常 [Errno 24] Too many open files
(要查看它,请将行 executor.submit(super().f)
替换为 print(executor.submit(super().f).exception())
)。
但是,将 ProcessPoolExecutor
替换为 ThreadPoolExecutor
会按预期打印“called”。
2。为什么以下使用 multiprocessing.pool
模块的 Python 代码会引发异常 AssertionError: daemonic processes are not allowed to have children
?
import multiprocessing.pool
class A:
def f(self):
print("called")
class B(A):
def f(self):
pool = multiprocessing.pool.Pool(2)
pool.apply(super().f)
if __name__ == "__main__":
B().f()
但是,将 Pool
替换为 ThreadPool
会按预期打印“called”。
环境:CPython3.7,MacOS 10.14。
concurrent.futures.ProcessPoolExecutor
和 multiprocessing.pool.Pool
使用 multiprocessing.queues.Queue
将工作函数对象从调用者传递到工作进程,Queue
使用 pickle
模块到 serialize/unserialize,但未能正确处理具有子 class 实例的绑定方法对象:
f = super().f
print(f)
pf = pickle.loads(pickle.dumps(f))
print(pf)
输出:
<bound method A.f of <__main__.B object at 0x104b24da0>>
<bound method B.f of <__main__.B object at 0x104cfab38>>
A.f
变为 B.f
,这实际上在工作进程中创建无限递归调用 B.f
到 B.f
。
pickle.dumps
利用绑定方法对象的__reduce__
方法,IMO,its implementation,没有考虑到这种场景,不关心真正的func
对象,但仅尝试使用简单名称 (f
) 从实例 self
obj (B()
) 返回,这导致 B.f
,很可能是一个错误。
好消息是,我们知道问题出在哪里,我们可以通过实现我们自己的缩减函数来解决它,该函数尝试从原始函数 (A.f
) 和实例对象 ( B()
):
import types
import copyreg
import multiprocessing
def my_reduce(obj):
return (obj.__func__.__get__, (obj.__self__,))
copyreg.pickle(types.MethodType, my_reduce)
multiprocessing.reduction.register(types.MethodType, my_reduce)
我们可以这样做,因为绑定方法是一个描述符。
ps: 我已经提交了 a bug report.
1.为什么以下使用 concurrent.futures
模块的 Python 代码永远挂起?
import concurrent.futures
class A:
def f(self):
print("called")
class B(A):
def f(self):
executor = concurrent.futures.ProcessPoolExecutor(max_workers=2)
executor.submit(super().f)
if __name__ == "__main__":
B().f()
该调用引发了一个不可见的异常 [Errno 24] Too many open files
(要查看它,请将行 executor.submit(super().f)
替换为 print(executor.submit(super().f).exception())
)。
但是,将 ProcessPoolExecutor
替换为 ThreadPoolExecutor
会按预期打印“called”。
2。为什么以下使用 multiprocessing.pool
模块的 Python 代码会引发异常 AssertionError: daemonic processes are not allowed to have children
?
import multiprocessing.pool
class A:
def f(self):
print("called")
class B(A):
def f(self):
pool = multiprocessing.pool.Pool(2)
pool.apply(super().f)
if __name__ == "__main__":
B().f()
但是,将 Pool
替换为 ThreadPool
会按预期打印“called”。
环境:CPython3.7,MacOS 10.14。
concurrent.futures.ProcessPoolExecutor
和 multiprocessing.pool.Pool
使用 multiprocessing.queues.Queue
将工作函数对象从调用者传递到工作进程,Queue
使用 pickle
模块到 serialize/unserialize,但未能正确处理具有子 class 实例的绑定方法对象:
f = super().f
print(f)
pf = pickle.loads(pickle.dumps(f))
print(pf)
输出:
<bound method A.f of <__main__.B object at 0x104b24da0>>
<bound method B.f of <__main__.B object at 0x104cfab38>>
A.f
变为 B.f
,这实际上在工作进程中创建无限递归调用 B.f
到 B.f
。
pickle.dumps
利用绑定方法对象的__reduce__
方法,IMO,its implementation,没有考虑到这种场景,不关心真正的func
对象,但仅尝试使用简单名称 (f
) 从实例 self
obj (B()
) 返回,这导致 B.f
,很可能是一个错误。
好消息是,我们知道问题出在哪里,我们可以通过实现我们自己的缩减函数来解决它,该函数尝试从原始函数 (A.f
) 和实例对象 ( B()
):
import types
import copyreg
import multiprocessing
def my_reduce(obj):
return (obj.__func__.__get__, (obj.__self__,))
copyreg.pickle(types.MethodType, my_reduce)
multiprocessing.reduction.register(types.MethodType, my_reduce)
我们可以这样做,因为绑定方法是一个描述符。
ps: 我已经提交了 a bug report.