PyInstaller-построенный Windows EXE не работает с многопроцессорной обработкой

В моем проекте я использую библиотеку Python multiprocessing для создания нескольких процессов в __main__. Проект упаковывается в один Windows EXE с использованием PyInstaller 2.1.1.

Я создаю новые процессы следующим образом:

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'

Мое разочарование в этом "решении" заключается в том, что, совершенно неясно, что именно он исправляет, и, во-вторых, он написал таким запутанным способом, что становится невозможным вывести, какие части являются решением, а какие являются лишь иллюстрацией.

Может ли кто-нибудь осветить эту проблему и дать понять, что именно нужно изменить в проекте, который позволяет многопроцессорно работать в исполняемых файлах с одним файлом Windows PyInstaller?

Ответ 1

Отвечая на мои собственные вопросы после нахождения этого билета PyInstaller:

Очевидно, все, что нам нужно сделать, это предоставить класс Process_Popen), как показано ниже, и использовать его вместо multiprocessing.Process. Я исправил и упростил класс для работы только с Windows, * ix-системам может понадобиться другой код.

Для полноты здесь адаптированный образец из вышеуказанного вопроса:

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

Ответ 2

Чтобы добавить к 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, незначительного. Поэтому вы должны просто придерживаться простого вызова multiprocessing.freeze_support() сразу после if __name__ == '__main__':.

Если вы планируете использовать --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 многопроцессорности. Они добавляют os.sep к своей переменной окружения _MEIPASS2. (Строка: 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

Error when using os.sep in _MEIPASS2

Код, указанный выше, тот же, без os.sep. Удаление os.sep устраняет эту проблему и позволяет выполнять многопроцессорную работу с использованием конфигурации --onefile.

Вкратце:

Включение поддержки многопроцессорности --onedir в Windows (не работает с --onefile в Windows, но в остальном безопасно на всех платформах/конфигурациях):

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

Разрешение --onefile поддержки многопроцессорности в Windows (безопасно на всех платформах/конфигурациях, совместимо с --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 Recipe, Документы многопроцессорности Python