ThreadPoolExecutor、ProcessPoolExecutor 和全局变量

ThreadPoolExecutor, ProcessPoolExecutor and global variables

总的来说,我是并行化的新手,尤其是 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 参数,并且有很多。因此,似乎不推荐将它们作为参数传递。那么如何将这些全局变量正确传递给每个 process/thread?

我知道我可以改变周围的事情,这会奏效,但我不明白为什么。例如。以下适用于两个执行器,但这也意味着必须对每个实例进行全局初始化。

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()

所以我的主要问题是,实际发生了什么。为什么这段代码适用于线程而不适用于进程?而且,我如何正确地将 set globals 传递给每个 process/thread 而不必为每个实例重新初始化它们?

(旁注:因为我读到 concurrent.futures 在 Windows 上的行为可能有所不同,我必须注意我在 运行 Python 3.6 36=] 10 64 位。)

我不确定这种方法的局限性,但您可以在主 process/thread 之间传递(可序列化?)对象。这也将帮助您摆脱对全局变量的依赖:

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,您在 [=13] 中得到的 opts 字典=] 将是一个冻结的副本,因此对它的更改将不会跨进程可见,一旦您 return 到 __main__ 块,它们也将不可见。 ThreadPoolExecutor,另一方面,将在线程之间共享 dict 对象。

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

所以当你使用线程时,你正在为你当前的盒子(主进程)创建多个工人。但是当你使用过程时,你正在创建另一个盒子。在这种情况下,这个盒子中初始化的全局变量与另一个盒子中的全局变量完全不同。这就是为什么它没有像您预期的那样工作。

jedwards给出的解决方案在大多数情况下已经足够好了。可以显式地将当前box中的资源打包(序列化变量)传递给另一个box(传输到另一个进程),让那个box中的worker可以访问资源。

一个进程表示 activity 即 运行 在一个单独的进程中 OS 术语的含义,而线程所有 运行 在您的主进程中。每个进程都有自己独特的命名空间。

您的主进程通过在您的 __name__ == '__main__'condition 中调用 init() 为其自己的命名空间将值设置为 greeting。在你的新进程中,这不会发生(__name__ 在这里是 '__mp_name__')因此 greeting 仍然是 None 并且 init() 永远不会被调用,除非你明确地这样做在您的进程执行的函数中。

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

您可能还想查看文档 Sharing State Between Processes

实际上,OP 的第一个代码将按预期在 Linux 上运行(在 Python 3.6-3.8 中测试)因为

On Unix a child process can make use of a shared resource created in a parent process using a global resource.

如多处理 doc 中所述。然而,出于某种神秘的原因,它不适用于我的 Mac 运行 Mojave(应该是 UNIX-compliant OS;仅通过 [=17= 测试] 3.8).可以肯定的是,它不会在 Windows 上运行,并且通常不推荐使用多个进程。