首页 > 解决方案 > Python 并发期货 ProcessPoolExecutor 和全局变量:在 Linux 上工作,在 MacOS 上出错

问题描述

下面的代码示例按照我认为应该在两台 Linux 机器上运行:在运行 Red Hat 4.8.5-39 内核的大型基于 CentOS 的服务器上使用 Python 3.6.8,并在我的基于 MX 的机器上运行 Python 3.7.3 Debian 8.3.0-6 内核)。

$ python3 testshared.py filename.dat
filename.dat
270623586670000.0

但是,在我的 Mac 上运行 Mojave 10.14.6,使用 Python 3.8.3,我收到一个错误,因为foo=[]在 function 中processBigFatRow()。注意是在启动进程池之前foo分配的。就像在 Linux 中一样,assign in的版本被传递给进程,而在 Mac 上,进程只使用代码顶部的初始化(我必须把它放在那里,所以它们是可变的)。getBigFatData() foogetBigFatData()global

我了解该进程是主进程的“独立副本”,您不能在一个进程中分配全局变量并期望它们在另一个进程中发生变化。但是,在并行进程启动之前已经设置的变量,并且仅作为参考使用呢?就像跨操作系统的进程副本不一样。哪个“按设计”工作?

代码示例:

import pylab as pl
from concurrent import futures
import sys

foo = []
bar = []

def getBigFatData(filename):
    
    global foo, bar
    # get the big fat data
    print(filename)
    foo = pl.arange(1000000).reshape(1000,1000)
    # compute something as a result
    bar = pl.sum(foo, axis=1)

def processBigFatRow(row):
    total = pl.sum(foo[row,:]**2) if row % 5 else bar[row] 
    return total
    
def main():
    
    getBigFatData(sys.argv[1])
    
    grandTotal = 0.
    rows = pl.arange(100)
    with futures.ProcessPoolExecutor() as pool:
        for tot in pool.map(processBigFatRow, rows):
            grandTotal+=tot
    
    print(grandTotal)

if __name__ == '__main__':
    main()

编辑:

正如建议的那样,我在我的 MX-Linux 机器上测试了 Python 3.8.6,它可以工作。

所以它适用于使用 Python 3.6.8、3.7.3 和 3.8.6 的 Linux。但它不在使用 Python 3.8.3 的 Mac 上。

编辑2:

来自多处理文档

在 Unix 上,子进程可以使用在父进程中使用全局资源创建的共享资源。

所以它不能在 Windows 上运行(这不是最佳实践),但它不应该在 Mac 上运行吗?

标签: pythonlinuxmacosglobal-variablesconcurrent.futures

解决方案


这是因为,在 MacOS 上,默认的多处理启动方法在 Python 3.8中发生了变化。它从fork(py37) 变为spawn(py38),导致其咬牙切齿。

在 3.8 版更改: 在 macOS 上,spawn启动方法现在是默认方法。start 方法应该被认为是不安全的fork,因为它可能导致子进程崩溃。请参阅 bpo-33725

With spawn: 全局变量不与多进程进程共享。

因此,实际上,作为一种快速修复,'fork'在您的所有调用中指定一个上下文ProcessPoolExecutor,使用mp.get_context('fork'). 但请注意上述警告;一个长期的解决方案是使用多处理文档中列出的一种技术来共享变量。

例如,在上面的代码中,替换:

with ProcessPoolExecutor() as pool:
    ...

和:

import multiprocessing as mp

with ProcessPoolExecutor(mp_context=mp.get_context('fork')) as executor:
    ...

替代方案

当您只是编写一两个小脚本,并确定没有人在某处使用不同的地方调用您的代码时,您可以在代码块main中一劳永逸地设置默认启动方法:mainmp.set_start_method

if __name__ == '__main__':
    mp.set_start_method('fork')
    ...

但总的来说,我更喜欢第一种方法,因为您不必假设调用者事先设置了 start 方法。而且,根据文档:

请注意,这应该最多调用一次,并且应该if __name__ == '__main__'在主模块的子句中受到保护。


推荐阅读