使用PyInstaller构建的Windows EXE在多进程时失败

27 投票
2 回答
26516 浏览
提问于 2025-04-18 14:40

在我的项目中,我使用了Python的multiprocessing库来创建多个进程,代码是在__main__中运行的。这个项目正在使用PyInstaller 2.1.1打包成一个单独的Windows可执行文件(EXE)。

我创建新进程的方式是这样的:

from multiprocessing import Process
from Queue import Empty

def _start():
    while True:
        try:
            command = queue.get_nowait()
        # ... and some more code to actually interpret commands
        except Empty:
            time.sleep(0.015)

def start():
    process = Process(target=_start, args=args)
    process.start()
    return process

然后在__main__中:

if __name__ == '__main__':
    freeze_support()

    start()

不幸的是,当我把应用打包成EXE并启动时,我在这一行遇到了WindowsError 5或6(看起来是随机的):

command = queue.get_nowait()

PyInstaller官网上有个说明说,我需要修改我的代码,以便在打包应用为单个文件时启用Windows的多进程功能。

我在这里重现了代码:

import multiprocessing.forking
import os
import sys


class _Popen(multiprocessing.forking.Popen):
    def __init__(self, *args, **kw):
        if hasattr(sys, 'frozen'):
            # We have to set original _MEIPASS2 value from sys._MEIPASS
            # to get --onefile mode working.
            # Last character is stripped in C-loader. We have to add
            # '/' or '\\' at the end.
            os.putenv('_MEIPASS2', sys._MEIPASS + os.sep)
        try:
            super(_Popen, self).__init__(*args, **kw)
        finally:
            if hasattr(sys, 'frozen'):
                # On some platforms (e.g. AIX) 'os.unsetenv()' is not
                # available. In those cases we cannot delete the variable
                # but only set it to the empty string. The bootloader
                # can handle this case.
                if hasattr(os, 'unsetenv'):
                    os.unsetenv('_MEIPASS2')
                else:
                    os.putenv('_MEIPASS2', '')


class Process(multiprocessing.Process):
    _Popen = _Popen


class SendeventProcess(Process):
    def __init__(self, resultQueue):
        self.resultQueue = resultQueue

        multiprocessing.Process.__init__(self)
        self.start()

    def run(self):
        print 'SendeventProcess'
        self.resultQueue.put((1, 2))
        print 'SendeventProcess'


if __name__ == '__main__':
    # On Windows calling this function is necessary.
    if sys.platform.startswith('win'):
        multiprocessing.freeze_support()
    print 'main'
    resultQueue = multiprocessing.Queue()
    SendeventProcess(resultQueue)
    print 'main'

我对这个“解决方案”感到很沮丧,首先,它完全不清楚到底修补了什么,其次,它写得非常复杂,让人根本无法分辨哪些部分是解决方案,哪些只是示例。

有没有人能帮我解释一下这个问题,并提供一些关于在PyInstaller构建的单文件Windows可执行文件中,究竟需要改变哪些内容才能启用多进程的见解?

2 个回答

39

接着nikola的回答说...

*nix系统(比如Linux、Mac OS X等)不需要做任何改动就能让PyInstaller正常工作。(这包括--onedir--onefile这两个选项。)如果你只打算支持*nix系统,那就不用担心这些问题。

不过,如果你打算支持Windows系统,就需要根据你选择的选项(--onedir--onefile)添加一些代码。

如果你打算使用--onedir,你只需要添加一个特殊的方法调用:

if __name__ == '__main__':
    # On Windows calling this function is necessary.
    multiprocessing.freeze_support()

根据文档,这个调用必须在if __name__ == '__main__':之后立即进行,否则就无法正常工作。(强烈建议你在主模块中包含这两行代码。)

不过实际上,你可以在调用之前做个检查,这样也能正常工作:

if __name__ == '__main__':
    if sys.platform.startswith('win'):
        # On Windows calling this function is necessary.
        multiprocessing.freeze_support()

另外,调用multiprocessing.freeze_support()在其他平台和情况下也是可以的——运行它只会影响Windows的冻结支持。如果你对字节码感兴趣,你会发现这个if语句会增加一些字节码,因此使用if语句带来的潜在节省几乎可以忽略不计。所以,建议你在if __name__ == '__main__':之后直接调用multiprocessing.freeze_support()

如果你打算使用--onefile,你需要添加nikola的代码:

import multiprocessing.forking
import os
import sys

class _Popen(multiprocessing.forking.Popen):
    def __init__(self, *args, **kw):
        if hasattr(sys, 'frozen'):
            # We have to set original _MEIPASS2 value from sys._MEIPASS
            # to get --onefile mode working.
            os.putenv('_MEIPASS2', sys._MEIPASS)
        try:
            super(_Popen, self).__init__(*args, **kw)
        finally:
            if hasattr(sys, 'frozen'):
                # On some platforms (e.g. AIX) 'os.unsetenv()' is not
                # available. In those cases we cannot delete the variable
                # but only set it to the empty string. The bootloader
                # can handle this case.
                if hasattr(os, 'unsetenv'):
                    os.unsetenv('_MEIPASS2')
                else:
                    os.putenv('_MEIPASS2', '')

class Process(multiprocessing.Process):
    _Popen = _Popen

# ...

if __name__ == '__main__':
    # On Windows calling this function is necessary.
    multiprocessing.freeze_support()

    # Use your new Process class instead of multiprocessing.Process

你可以把上面的代码和他其他的代码结合起来,或者使用以下代码:

class SendeventProcess(Process):
    def __init__(self, resultQueue):
        self.resultQueue = resultQueue

        multiprocessing.Process.__init__(self)
        self.start()

    def run(self):
        print 'SendeventProcess'
        self.resultQueue.put((1, 2))
        print 'SendeventProcess'

if __name__ == '__main__':
    # On Windows calling this function is necessary.
    multiprocessing.freeze_support()

    print 'main'
    resultQueue = multiprocessing.Queue()
    SendeventProcess(resultQueue)
    print 'main'

我从这里获得了代码,这是PyInstaller关于多进程的最新网站。(他们似乎关闭了基于Trac的网站。)

注意,他们在--onefile的多进程支持代码中有一个小错误。他们在_MEIPASS2环境变量中添加了os.sep。(代码行:os.putenv('_MEIPASS2', sys._MEIPASS + os.sep))这会导致问题:

  File "<string>", line 1
    sys.path.append(r"C:\Users\Albert\AppData\Local\Temp\_MEI14122\")
                                                                    ^
SyntaxError: EOL while scanning string literal

使用os.sep时在_MEIPASS2中的错误

我提供的代码是相同的,但没有os.sep。去掉os.sep就能解决这个问题,并且让多进程在--onefile配置下正常工作。

总结:

在Windows上启用--onedir的多进程支持(在Windows上--onefile不支持,但在所有平台/配置上都安全):

if __name__ == '__main__':
    # On Windows calling this function is necessary.
    multiprocessing.freeze_support()

在Windows上启用--onefile的多进程支持(在所有平台/配置上都安全,且与--onedir兼容):

import multiprocessing.forking
import os
import sys

class _Popen(multiprocessing.forking.Popen):
    def __init__(self, *args, **kw):
        if hasattr(sys, 'frozen'):
            # We have to set original _MEIPASS2 value from sys._MEIPASS
            # to get --onefile mode working.
            os.putenv('_MEIPASS2', sys._MEIPASS)
        try:
            super(_Popen, self).__init__(*args, **kw)
        finally:
            if hasattr(sys, 'frozen'):
                # On some platforms (e.g. AIX) 'os.unsetenv()' is not
                # available. In those cases we cannot delete the variable
                # but only set it to the empty string. The bootloader
                # can handle this case.
                if hasattr(os, 'unsetenv'):
                    os.unsetenv('_MEIPASS2')
                else:
                    os.putenv('_MEIPASS2', '')

class Process(multiprocessing.Process):
    _Popen = _Popen

# ...

if __name__ == '__main__':
    # On Windows calling this function is necessary.
    multiprocessing.freeze_support()

    # Use your new Process class instead of multiprocessing.Process

来源:PyInstaller食谱Python多进程文档

9

我在找到这个PyInstaller的讨论帖后,决定自己回答自己的问题:

显然,我们只需要提供一个Process(还有_Popen)类,如下所示,然后用它来替代multiprocessing.Process。我已经对这个类进行了修正和简化,使其仅适用于Windows系统,其他操作系统可能需要不同的代码。

为了完整起见,这里是从上面问题中改编的示例:

import multiprocessing
from Queue import Empty

class _Popen(multiprocessing.forking.Popen):
    def __init__(self, *args, **kw):
        if hasattr(sys, 'frozen'):
            os.putenv('_MEIPASS2', sys._MEIPASS)
        try:
            super(_Popen, self).__init__(*args, **kw)
        finally:
            if hasattr(sys, 'frozen'):
                os.unsetenv('_MEIPASS2')


class Process(multiprocessing.Process):
    _Popen = _Popen


def _start():
    while True:
        try:
            command = queue.get_nowait()
        # ... and some more code to actually interpret commands
        except Empty:
            time.sleep(0.015)

def start():
    process = Process(target=_start, args=args)
    process.start()
    return process

撰写回答