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