使用PyInstaller构建的Windows EXE在多进程时失败
在我的项目中,我使用了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 个回答
接着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
。去掉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的讨论帖后,决定自己回答自己的问题:
显然,我们只需要提供一个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