ThreadPoolExecutor、ProcessPoolExecutor和全局变量

6

我对并行化和concurrent.futures都很陌生。我想要测试我的脚本并比较使用线程和进程的差异,但是我发现当我使用ProcessPoolExecutor时,我无法使用全局变量。

以下代码将会输出我期望的Hello,但是当你把ThreadPoolExecutor改为ProcessPoolExecutor时,它将会输出None

from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor

greeting = None

def process():
    print(greeting)

    return None


def main():
    with ThreadPoolExecutor(max_workers=1) as executor:
        executor.submit(process)

    return None


def init():
    global greeting
    greeting = 'Hello'

    return None

if __name__ == '__main__':
    init()
    main()

我不明白为什么会这样。在我的实际程序中,init用于将全局变量设置为CLI参数,并且有很多这样的变量。因此,将它们作为参数传递似乎不是推荐的方法。那么我该如何正确地将这些全局变量传递给每个进程/线程呢?
我知道我可以改变代码,使其正常工作,但我不明白为什么。例如,以下代码对于执行器都有效,但这也意味着全局初始化必须发生在每个实例中。
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor

greeting = None

def init():
    global greeting
    greeting = 'Hello'

    return None


def main():
    with ThreadPoolExecutor(max_workers=1) as executor:
        executor.submit(process)

    return None

def process():
    init()
    print(greeting)

    return None

if __name__ == '__main__':
    main()

所以我的主要问题是,到底发生了什么。为什么这段代码可以使用线程而不能使用进程?如何正确地将设置好的全局变量传递给每个进程/线程而不必为每个实例重新初始化它们?

(附注:因为我已经读到concurrent.futures在Windows上的行为可能会有所不同,所以我必须说明我正在运行Python 3.6,Windows 10 64位操作系统。)

4个回答

4

我不确定这种方法的限制,但是您可以在主进程/线程之间传递(可序列化的?)对象。这也有助于消除对全局变量的依赖:

from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor

def process(opts):
    opts["process"] = "got here"
    print("In process():", opts)

    return None


def main(opts):
    opts["main"] = "got here"
    executor = [ProcessPoolExecutor, ThreadPoolExecutor][1]
    with executor(max_workers=1) as executor:
        executor.submit(process, opts)

    return None


def init(opts):                         # Gather CLI opts and populate dict
    opts["init"] = "got here"

    return None


if __name__ == '__main__':
    cli_opts = {"__main__": "got here"} # Initialize dict
    init(cli_opts)                      # Populate dict
    main(cli_opts)                      # Use dict

适用于两种执行器类型。

编辑:虽然听起来对您的使用情况不构成问题,但我要指出,在使用 ProcessPoolExecutor 时,您在 process 中获取的 opts 字典将是一个冻结的副本,因此对它的更改将不会跨进程可见,也不会在返回到 __main__ 块后可见。另一方面,ThreadPoolExecutor 将在线程之间共享字典对象。


你能看一下这个问题吗?https://stackoverflow.com/questions/59040311/update-variable-while-working-with-processpoolexecutor?noredirect=1#comment104324638_59040311 - johnrao07

2
实际上,OP的第一段代码在Linux上可以按预期工作(已在Python 3.6-3.8中测试),因为如doc中所解释的那样,“在Unix中,子进程可以使用由父进程创建的共享资源,使用全局资源”。然而,由于某些神秘的原因,它在我的运行Mojave的Mac上无法工作(这应该是一个符合UNIX标准的操作系统;仅在Python 3.8下测试)。当然,在Windows上它也不会工作,并且通常不建议在多个进程中使用。

0

让我们想象一个进程是一个盒子,而线程是盒子里的工人。工人只能访问盒子中的资源,不能触碰其他盒子中的资源。

因此,当您使用线程时,您正在为当前盒子(主进程)创建多个工人。但是,当您使用进程时,您正在创建另一个盒子。在这种情况下,在该盒子中初始化的全局变量与另一个盒子中的变量完全不同。这就是为什么它不按照您的预期工作的原因。

jedwards提供的解决方案对于大多数情况已足够好。您可以明确地将当前盒子中的资源打包(序列化变量),并将其传递到另一个盒子中(传输到另一个进程),以使该盒子中的工人可以访问这些资源。


这个解决方案不够好的情况怎么办?例如,对象太大而无法进行序列化且会导致严重的性能损失的情况?那么剩下的唯一解决方案是使用全局变量吗? - Jivan
1
其实,大多数时候我们是没有选择的。在一台电脑上,我们可以使用共享内存。但是大多数情况下,我们都在不同的机器上。我们必须支付那个成本。 - Sraw

0

进程代表在操作系统中以术语意义运行的独立进程中运行的活动,而线程都在主进程中运行。每个进程都有自己独特的命名空间。

您的主进程通过在其自己的命名空间中调用init()来设置greeting的值,条件为__name__ == '__main__'。在新进程中,这种情况不会发生(这里的__name__'__mp_name__'),因此greeting仍然为None,并且除非您在进程执行的函数中明确这样做,否则实际上不会调用init()

虽然通常不建议在进程之间共享状态,但有一些方法可以实现,如@jedwards的回答中所概述的那样。

您还可以查看文档中的Sharing State Between Processes


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接