作者:灵剑
链接:https://www.zhihu.com/question/61617396/answer/189453329
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

这个跟multiprocessing的原理有关,也涉及到Python中pickling的一些实现机制。

首先要说明multiprocessing模块现在有多种实现原理,类Unix系统默认使用fork,原理在于创建Process并启动的时候进行一次fork,然后子进程执行Process中指定的函数,它可以继承创建进程时内存中的对象,因此可以指定各种对象给子进程如函数、闭包、socket等

而Python2中Windows操作系统的实现spawn,每次重新启动一个全新的Python进程。以及Python3中forkserver的实现(在multiprocessing初始化的时候通过fork创建一个forkserver的进程,forkserver进程不再执行后续的代码,而是等待请求;所有后续创建multiprocessing进程时,都连接forkserver,由forkserver进行fork,产生一个“干净”的进程来作为新进程),这两种实现并不继承当前运行环境中的对象,所以需要使用的对象必须通过pickling的方法传递给子进程。

另外,multiprocessing中的Pool对象,它的原理在于预先创建好许多进程,然后从主进程中接受任务。这些任务因为不是在创建进程(fork)时创建出的,所以必须通过pickling的机制传递给子进程。multiprocessing.Queue和multiprocessing.Pipe也同样是使用pickling模块在进程间传递Python对象。

接下来就要说一下pickling了,pickling模块可以将Python对象序列化成字节流,再反序列化回到Python对象。但是它是有一定限制的,并不是所有的对象都可以进行序列化。函数和类在pickling中是不能“直接”序列化的,它们在pickling中序列化的原理在于将函数和类变为:package.module.func_name这样的字符串,即模块路径 + 函数/类名的形式。这就要求反序列化的时候:

  1. 这个模块可以被import
  2. import后的这个模块中有这个全局名称
  3. 而且全局名称代表的值就是传递的值。

然而使用装饰器修饰的时候,main这个函数其实被修改了一次,变为了修饰之后的函数,但是未修饰的函数和修饰后的函数都共享了__main__.main这个名称,这导致传递的虽然是修饰前的函数,但实际接收方接受了__main__.main这个名称,然后重新import得到的是修饰后的函数,所以在子进程中执行的也是修饰后的函数。multiprocessing模块有个安全机制,禁止在multiprocessing创建的子进程中再创建multiprocessing子进程,因此这个修饰过的函数会在.Pool的地方报错,这个异常没有传递回主进程,所以看上去就像什么都没有做一样。

Last modification:December 23, 2021
如果觉得我的文章对你有用,请随意赞赏