El EXE de Windows construido por PyInstaller falla con multiprocesamiento

En mi proyecto, estoy usando la biblioteca de multiprocessing de Python para crear múltiples procesos en __main__. El proyecto se está empaquetando en un solo EXE de Windows usando PyInstaller 2.1.1.

Creo nuevos procesos así:

 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 

Y en __main__:

 if __name__ == '__main__': freeze_support() start() 

Desafortunadamente, al empaquetar la aplicación en un EXE y lanzarla, obtengo WindowsError 5 o 6 (parece aleatorio) en esta línea:

 command = queue.get_nowait() 

Una receta en la página de inicio de PyInstaller dice que debo modificar mi código para habilitar el multiprocesamiento en Windows al empaquetar la aplicación como un solo archivo.

Estoy reproduciendo el código aquí:

 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 (eg 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' 

Mi frustración con esta “solución” es que, uno, no está del todo claro qué es exactamente lo que está parcheando, y, dos, que está escrito de una manera tan complicada que resulta imposible inferir qué partes son la solución y cuáles son solo una ilustración.

¿Alguien puede compartir algo de luz sobre este problema y proporcionar una idea de qué se necesita cambiar exactamente en un proyecto que permita el multiprocesamiento en los ejecutables de Windows de un solo archivo PyInstaller?

Para agregar a la respuesta de Nikola …

* nix (Linux, Mac OS X, etc.) NO requiere ningún cambio para que PyInstaller funcione. (Esto incluye las opciones --onedir y --onefile ). Si solo tiene la intención de admitir los sistemas * nix, no debe preocuparse por nada de esto.

Sin embargo, si planea ser compatible con Windows, deberá agregar algún código, dependiendo de la opción que elija: --onedir o --onefile .

Si planea usar --onedir , todo lo que deberá agregar es un método especial llamado:

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

De acuerdo con la documentación, esta llamada debe hacerse inmediatamente después if __name__ == '__main__': o de lo contrario no funcionará. (Se recomienda encarecidamente que tenga estas dos líneas en su módulo principal).

Sin embargo, en realidad, puede permitirse hacer una verificación antes de la llamada, y las cosas seguirán funcionando:

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

Sin embargo, llamar a multiprocessing.freeze_support() es posible en otras plataformas y situaciones, ya que ejecutarlo solo afecta al soporte de congelación en Windows. Si eres un fanático de los códigos de bytes, notarás que la statement if agrega un código de bytes y hace que los ahorros potenciales de usar una statement if sean insignificantes. Por lo tanto, debe limitarse a una llamada multiprocessing.freeze_support() inmediatamente después de if __name__ == '__main__':

Si planea usar --onefile , deberá agregar el código de 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 (eg 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 

Puede combinar lo anterior con el rest de su código, o lo siguiente:

 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' 

Obtuve el código de aquí , el nuevo sitio de PyInstaller para la receta de multiprocesamiento. (Parece que han cerrado su sitio basado en Trac).

Tenga en cuenta que tienen un pequeño error en su código para --onefile soporte de multiprocesamiento de --onefile . Añaden os.sep a su variable de entorno _MEIPASS2 . (Línea: os.putenv('_MEIPASS2', sys._MEIPASS + os.sep) ) Esto rompe cosas:

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

Error al usar os.sep en _MEIPASS2

El código que proporcioné arriba es el mismo, sin el os.sep . La eliminación de os.sep soluciona este problema y permite que el multiprocesamiento funcione con la configuración --onefile .

En resumen:

Habilitar el --onedir multiprocesamiento en Windows (no funciona con el --onefile en Windows, pero es seguro en todas las plataformas / configuraciones):

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

Habilitación del --onefile multiproceso --onefile en Windows (seguro en todas las plataformas / configuraciones, compatible con --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 (eg 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 

Fuentes: receta de PyInstaller , documentos de multiprocesamiento de Python

Respondiendo a mis propias preguntas después de encontrar este boleto de PyInstaller :

Al parecer, todo lo que tenemos que hacer es proporcionar una clase de Process (y _Popen ) como se muestra a continuación, y utilizarla en lugar de multiprocessing.Process . He corregido y simplificado la clase para que funcione solo en Windows, * los sistemas ix pueden necesitar un código diferente.

En aras de la integridad, aquí está la muestra adaptada de la pregunta anterior:

 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