跳转至主要内容

Python编程

Python多进程在Windows 和 Linux 的区别

Sprite
发表于 2024年10月18日

多进程处理在 Windows 和 Linux 上的行为的有些不同,当相同代码在不同平台上运行时,可能会产生不一致的结果。

这篇文章将跟大家讲解下它们之间的区别以及如何避免错误。

首先,展示如何使用多进程的最快方法是运行一个简单的函数,而不会阻塞主程序

import multiprocessing as mp
from time import sleep

def simple_func():
    print('开始 simple func')
    sleep(1)
    print('结束 simple func')

if __name__ == '__main__':
    p = mp.Process(target=simple_func)
    p.start()
    print('等待 simple_func')
    p.join()

输出如下:

等待 simple_func
开始 simple func
结束 simple func

输出结果符合我们的预期。

我们再看看下面这一段代码

import multiprocessing as mp
from time import sleep

print('定义 simple_func 之前')

def simple_func():
    print('开始 simple func')
    sleep(1)
    print('结束 simple func')

if __name__ == '__main__':
    p = mp.Process(target=simple_func)
    p.start()
    print('等待 simple_func')
    p.join()

如果我们在 Windows 上运行此代码,将得到以下输出:

定义 simple_func 之前
等待 simple_func
定义 simple_func 之前
开始 simple func
结束 simple func

在 Linux 上,我们会得到以下输出:

定义 simple_func 之前
等待 simple_func
开始 simple func
结束 simple func

在 Linux 上,当你启动一个子进程时,它会被for。这意味着子进程继承了父进程的内存状态。然而,在 Windows 上(以及默认情况下在 Mac 上),进程是被生成的。这意味着一个新的解释器启动,代码重新运行。

它解释了为什么在 Windows 上运行代码会得到两行 定义 simple_func 之前 。你可能已经注意到,如果我们在文件末尾没有包含 if __main__ ,在 Windows 上,它会产生一个非常长的错误,最终以…结束。

RuntimeError:
        An attempt has been made to start a new process before the
        current process has finished its bootstrapping phase.

        This probably means that you are not using fork to start your
        child processes and you have forgotten to use the proper idiom
        in the main module:

            if __name__ == '__main__':
                freeze_support()
                ...

        The "freeze_support()" line can be omitted if the program
        is not going to be frozen to produce an executable.

在 Linux 上,它运行得很好。上面这个例子可能看起来并不起眼,但想象一下,如果你有一些计算密集型的初始化任务。也许在程序运行时你要进行一些系统检查。很可能你不想为每个启动的进程都运行所有这些检查。我们看一下下面这个例子

import multiprocessing as mp
import random

val = random.random()

def simple_func():
    print(val)

if __name__ == '__main__':
    print('multiprocessing 之前: ')
    simple_func()
    print('multiprocessing 之后:')
    p = mp.Process(target=simple_func)
    p.start()
    p.join()

在 Windows 上,它会给出如下输出,我们可以看到simple_func输出的两个值是不一样的

multiprocessing 之前:
0.16042209710776734
multiprocessing 之后:
0.9180213870647225

在 Linux 上,它会给出这样的输出,我们可以看到simple_func输出的两个值是一样的

multiprocessing 之前:
0.28832424513226507
multiprocessing 之后:
0.28832424513226507

这就将我们带到了最后一个话题,也是如何将在 Linux 上编写的代码移植到 Windows 。我们看一下下面这个例子

import multiprocessing as mp

class MyClass:
    def __init__(self, i):
        self.i = i

    def simple_method(self):
        print('这是一个简单函数')
        print(f'保存的值是: {self.i}')

    def mp_simple_method(self):
        self.p = mp.Process(target=self.simple_method)
        self.p.start()

    def wait(self):
        self.p.join()

if __name__ == '__main__':
    my_class = MyClass(1)
    my_class.mp_simple_method()
    my_class.wait()

上面的代码在 Linux 和 Windows 上都可以正常运行。但是假如你尝试进行一些稍微复杂的操作,比如从文件中写入或读取数据。

import multiprocessing as mp

class MyClass:
    def __init__(self, i):
        self.i = i
        self.file = open(f'{i}.txt''w')

    def simple_method(self):
        print('这是一个简单函数')
        print(f'保存的值是: {self.i}')

    def mp_simple_method(self):
        self.p = mp.Process(target=self.simple_method)
        self.p.start()

    def wait(self):
        self.p.join()
        self.file.close()

if __name__ == '__main__':
    my_class = MyClass(1)
    my_class.mp_simple_method()
    my_class.wait()

在 Linux 上,上述代码可以正常工作。然而,在 Windows(和 Mac)上,将会出现一个错误:

[...]
    ForkingPickler(file, protocol).dump(obj)
TypeError: cannot serialize '_io.TextIOWrapper' object

请注意,我们并未对文件做任何处理。我们只是打开并将其存储为类中的一个属性。但是,错误已经指向了一个特性。Spawning的工作方式是通过将整个对象进行 pickle。因此,如果我们有一个不能被 pickle 的类或属性,我们将无法使用它启动子进程。

有没有解决的办法?

遗憾的是,在 Windows 上没有改变进程启动方式的方法。另一方面,在 Linux 上可以改变进程启动方式。这将使您能够确保您的程序也在 Windows 和 Mac 上运行。我们只需要添加以下内容:

if __name__ == '__main__':
    mp.set_start_method('spawn')
    my_class = MyClass(1)
    my_class.mp_simple_method()
    my_class.wait()

通过使用 set_start_method ,程序将在 Linux 和 Windows 上产生相同的错误。这样能让代码在 Linux 和 Windows 保持结果的一致性。

所以,如果你曾经遇到这些不一致的情况,你将不得不重新思考你的程序设计,比如一些对象具有午饭pickle的属性,特别是设备驱动程序和 ZMQ 套接字。

速度也是需要考虑的因素

虽然多进程能利用计算机上的多个CPU来加快程序的速度,但启动每个进程可能是耗时的。在 Windows 和 Mac 上,Python 需要对对象进行 pickle 处理以创建子进程,这会增加开销,可能抵消了在独立进程上运行取得的好处,如果你的每个进程任务是小任务而非长时间运行任务时,多进程不一定能带来速度上的提升。我们这样想一下,如果启动子进程的所用的时间  > 进程任务的执行时间,那么会给CPU造成浪费。所以在使用多进程时,需要对任务进行合理的设计,不能让子进程任务的运行间过短。

写作不易,欢迎关注

评论已关闭。