PyInstaller 构建的 Windows EXE 因多处理而失败
Posted
技术标签:
【中文标题】PyInstaller 构建的 Windows EXE 因多处理而失败【英文标题】:PyInstaller-built Windows EXE fails with multiprocessing 【发布时间】:2014-09-16 15:34:14 【问题描述】:在我的项目中,我使用 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 可执行文件中启用多处理?
【问题讨论】:
配方能解决问题吗? 嗯,目前还不清楚(至少对我来说)如何应用这个配方。仅将上述代码粘贴到我的主要 Python 脚本中也不起作用,因为它会引发另外两个与我的 Python 脚本无关的异常。这告诉我配方存在根本缺陷。 如果您只是将配方作为独立脚本运行,它是否可以正常运行? 不,我收到两个错误,一个抱怨_MEIPASS2
格式不正确,另一个抱怨 pywintypes 有问题,这是不正确的。
什么版本的 PyInstaller?
【参考方案1】:
补充 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'
我从here 获得代码,这是 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
我上面提供的代码是一样的,没有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 Recipe、Python multiprocessing docs
【讨论】:
感谢您的详细回答。当我使用 --onefile 选项时,我在关闭我的主 Python 窗口(使用 tk)后出现僵尸线程的问题。您重新定义 Popen 的最后一个 sn-p 解决了该问题。对于使用 Python >3.4 的任何人,您需要使用import multiprocessing.popen_spawn_win32 as forking
而不是 multiprocessing.forking
。
不要忘记multiprocessing.freeze_support()
应该始终是__name__ == '__main__'
的第一行,并且在此行之前(即__name__ == '__main__'
之前)不应执行其他代码。我有一些导入执行了一些代码,导致multiprocessing.freeze_support()
没有任何效果。【参考方案2】:
找到this PyInstaller ticket后回答我自己的问题:
显然我们所要做的就是提供一个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
【讨论】:
票证链接失效。当前的文档在这里:github.com/pyinstaller/pyinstaller/wiki/Recipe-Multiprocessing以上是关于PyInstaller 构建的 Windows EXE 因多处理而失败的主要内容,如果未能解决你的问题,请参考以下文章
pyinstaller:确定 Windows 服务中 socket.io 和 aiohttp 的隐藏导入
使用 PyInstaller 在 --onefile 中使用 QML 构建 PyQt5
在 macOS 和 Windows 上使用 PyInstaller 编译时,简单的 PyQt5 GUI 看起来像 GTK
ORA-12638: 凭证检索失败使用 Windows 的 pyinstaller 捆绑 Python(pyQt + cx_Oracle) 应用程序