在Python中从线程调用多处理安全吗?


问题内容

根据
https://github.com/joblib/joblib/issues/180有没有一种安全的方法可以从python中的线程创建子进程?
Python多处理模块不允许在线程内使用。这是真的?

我的理解是,只要您不持有线程即可很好地从线程派生,而在执行时锁定(在当前线程中?在进程中的任何地方?)但是,Python的文档没有说明在fork之后是否安全共享threading.Lock对象。

还有一个问题:从日志记录模块共享的锁会导致fork问题。https://bugs.python.org/issue6721

我不确定这个问题是怎么产生的。听起来好像当前线程派生时,该进程中所有锁的状态都已复制到子进程中(这似乎是设计错误,并且肯定会死锁)。如果是这样,使用多重处理是否真的可以提供任何保护(因为我可以自由创建我的多重处理。在线程化之后创建池。其他线程创建并输入了锁,并且在线程启动后使用了非叉安全日志记录模块)-多处理模块文档也没有对multiprocessing.Pool应该分配在Locks之前保持沉默。

是否可以在各处用multiprocessing.Lock替换threading.Lock来避免此问题,并允许我们安全地组合线程和fork?


问题答案:

听起来好像当前线程派生时,该进程中所有锁的状态都已复制到子进程中(这似乎是设计错误,并且肯定会死锁)。

这不是设计错误,而是fork()早于单进程多线程。所有锁的状态都被复制到子进程中,因为它们只是内存中的对象。进程的整个地址空间都按fork复制。仅有一些不好的选择:要么通过fork复制
所有 线程,要么拒绝多线程应用程序中的派生。

因此,fork()除非在子进程之后execve()exit()在子进程中进行操作,否则永远都不是安全的事情。

是否可以在各处用multiprocessing.Lock替换threading.Lock来避免此问题,并允许我们安全地组合线程和fork?

否。 没有什么可以安全地组合线程和派生的,这是无法做到的。


问题是,当一个进程中有多个线程时,在fork()系统调用之后,您将无法继续在POSIX系统中安全地运行该程序。

例如,Linux手册fork(2)

* 在fork(2)进入多线程程序之后,子级只能安全地调用异步信号安全函数(请参阅参考资料signal(7)),直到它调用为止execve(2)

也就是说,可以fork()在多线程程序中然后仅调用异步信号安全的 C 函数(这是 C
函数的相当有限的子集),直到子进程已被另一个可执行文件替换!

子进程中的不安全C函数调用例如

  • malloc 用于动态内存分配
  • <stdio.h>格式化输入的任何功能
  • pthread_*线程状态处理所需的大多数功能,包括创建新线程…

因此,子进程实际上可以安全地执行的操作很少。不幸的是,CPython核心开发人员一直低估了由此引起的问题。甚至现在文档说:

请注意,安全地分叉多线程进程是 有问题的

委婉的说法是“不可能”。


如果您 使用start方法,则可以安全地 具有多个控制线程的Python进程中使用多重处理。在Python
3.4+中,现在可以更改start方法。在包括所有Python
2在内的早期Python版本中,POSIX系统始终表现为被指定为启动方法。这将导致不确定的行为。
__forkfork

问题不仅限于threading.Lock对象,还包括C标准库,C扩展等持有的 所有 锁。更糟糕的是,大多数时候人们会说“它对 我有用
…直到它停止工作。

甚至在某些情况下,看似单线程的Python程序实际上在MacOS X中是多线程的,在使用多处理时会导致失败和死锁。

另一个问题是,所有打开的文件句柄,它们的使用,共享套接字在派生的程序中可能表现得很奇怪,但是即使在单线程程序中也是如此。

TL; DR:multiprocessing在多线程程序中使用,带有C扩展名,打开的套接字等:

  • 如果您明确指定不是的启动方法,则在3.4+和POSIX中可以使用fork
  • 在Windows中很好,因为它不支持分支;
  • 在POSIX上的Python 2-3.3中:您几乎会用脚射击。