准确确定在 Python 多处理期间腌制的内容

Posted

技术标签:

【中文标题】准确确定在 Python 多处理期间腌制的内容【英文标题】:Determining exactly what is pickled during Python multiprocessing 【发布时间】:2021-10-18 17:37:29 【问题描述】:

正如线程What is being pickled when I call multiprocessing.Process? 中所解释的,在某些情况下,多处理几乎不需要通过酸洗传输数据。例如,在 Unix 系统上,解释器使用fork() 来创建进程,并且在 multiprocessing 启动时已经存在的对象可以被每个进程访问而无需进行酸洗。

但是,我正在尝试考虑“这就是它应该如何工作”之外的场景。例如,代码可能存在错误,并且本应为只读的对象被无意修改,导致其酸洗被转移到其他进程。

有什么方法可以确定在多处理过程中腌制了什么,或者至少腌制了多少?这些信息不一定必须是实时的,但如果有一种方法可以获得一些统计数据(例如,腌制的对象的数量),这可能会暗示为什么某些事情需要更长的时间来运行,这将是有帮助的由于意外的酸洗开销而超出预期。

我正在寻找 Python 环境内部的解决方案。进程跟踪(例如 Linux strace)、网络侦听、通用 IPC 统计信息以及可能用于计算进程之间移动字节数的类似解决方案,并不足以具体识别对象酸洗与其他类型的通信.


更新:令人失望的是,除了破解模块和/或解释器源之外,似乎没有办法收集酸洗统计信息。但是,@aaron 确实解释了这一点并澄清了一些小问题,所以我接受了答案。

【问题讨论】:

【参考方案1】:

Multiprocessing 并不完全是一个简单的库,但是一旦您熟悉了它的工作原理,就很容易四处寻找并弄清楚。

您通常希望从 context.py 开始。这是所有有用的类根据操作系统绑定的地方,并且......嗯......你活跃的“上下文”。有 4 个基本上下文:Fork、ForkServer 和 Spawn for posix;和一个单独的 Windows Spawn。这些又各自有自己的“Popen”(在start() 调用)来启动一个新进程来处理单独的实现。

popen_fork.py

创建一个进程实际上调用os.fork(),然后在子进程中组织运行BaseProcess._bootstrap(),它设置了一些清理内容,然后调用self.run() 来执行你给它的代码。 不会发生酸洗以这种方式启动进程,因为整个内存空间都会被复制(有一些例外。请参阅:fork(2))。

popen_spawn_xxxxx.py

我最熟悉 Windows,但我认为 win32 和 posix 版本的操作方式非常相似。一个新的 python 进程是用一个简单的命令行字符串创建的,包括一对管道句柄来读/写。新进程将导入 __main__ 模块(通常等于sys.argv[0]),以便访问所有需要的引用。然后它将执行一个简单的引导函数(来自命令字符串),尝试从创建它的管道中读取和un-pickle Process 对象。一旦它拥有Process 实例(一个新对象,它是一个副本;不仅仅是对原始对象的引用),它将再次安排调用_bootstrap()

popen_forkserver.py

第一次使用“forkserver”上下文创建新进程时,将“生成”一个新进程,运行一个处理新进程请求的简单服务器(侦听管道)。随后的流程请求都转到同一台服务器(基于导入机制和服务器实例的模块级全局)。然后从该服务器“分叉”新进程,以节省启动新 python 实例的时间。然而,这些新进程不能拥有任何相同的(如在同一个对象中而不是副本中)Process 对象,因为它们派生出来的 python 进程本身就是“生成”的。因此Process 实例被腌制和发送,就像使用“spawn”。这种方法的好处包括: 进行分叉的进程是单线程的,以避免死锁。启动一个新的 python 解释器的成本只支付一次。由于“fork”通常使用写时复制内存页面,解释器的内存消耗以及 __main__ 导入的任何模块都可以在很大程度上共享。


在所有情况下,一旦发生拆分,您应该考虑将内存空间完全分开,它们之间的唯一通信是通过管道或共享内存。锁和信号量由扩展库(用 c 编写)处理,但基本上是由操作系统管理的命名信号量。 Queue's、Pipe's 和 multiprocessing.Manager's 使用酸洗来同步对它们返回的代理对象的更改。新的multiprocessing.shared_memory 使用内存映射文件或缓冲区来共享数据(由操作系统管理,如信号量)。

解决您的问题:

代码可能存在错误,并且本应为只读的对象被无意修改,导致其酸洗转移到其他进程。

这仅适用于multiprocessing.Manager 代理对象。因为其他所有事情都要求您非常有意识地发送接收数据,或者使用其他传输机制而不是酸洗。

【讨论】:

关于我的最后一点。我主要是为了这个目的而避开经理。始终明确地从其他进程发送和接收数据要清楚得多。为此,我几乎总是使用队列。例如,当我有大量数据(图像帧等)时,发送和接收的效率一直很低,我有时会使用共享内存,但我会小心控制访问,所以并发部分的数组不是同时写入的。 我很欣赏它的彻底性,但正如我所说,我试图量化 腌制的内容,而不是直观地了解应该 腌制的内容。假设我正在使用队列来传递对象。对象已经存在,不应该被修改,所以酸洗应该是最小的。现在,哎呀,我错误地修改了一个对象,并将其放入队列中,因此必须对修改后的对象进行腌制。我希望我可以使用腌制量的测量来发现这个错误。 如果真的无法从pickle 模块中获取该信息,我可以接受“那不可能”的答案。 每次将某些内容放入队列时,都会对其进行序列化,然后使用 pickle 进行反序列化。它是否已更改并不重要。如果你愿意,你可以重新编写然后重新编译 _pickle 模块以记录到一个文件,但这很复杂,并且可能会破坏。 “假设我正在使用队列来传递对象。”因此它每次都被腌制。 queue.get 总是返回一个新对象,它实际上是 put 到队列中的副本。它永远不是“相同”的对象,因为单独的进程无法访问彼此的内存空间。否则,您将发送一个指向公共对象的指针。

以上是关于准确确定在 Python 多处理期间腌制的内容的主要内容,如果未能解决你的问题,请参考以下文章

python 属性错误:无法腌制本地对象。使用多处理

Python 多处理 PicklingError:无法腌制 <type 'function'>

Python多处理:AttributeError:无法腌制本地对象

Python多处理PicklingError:无法腌制<type'function'>

多处理,Python3,Windows:TypeError:无法腌制 _thread.lock 对象

Python多处理,不能腌制thread.lock(pymongo)