2014-07-24 6 views
10

Nel mio progetto sto usando la libreria Python multiprocessing per creare più processi in __main__. Il progetto viene impacchettato in un unico file EXE di Windows utilizzando PyInstaller 2.1.1.L'EXE di Windows generato da PyInstaller non funziona con il multiprocessing

io creo nuovi processi in questo modo:

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 

E in __main__:

if __name__ == '__main__': 
    freeze_support() 

    start() 

Purtroppo, quando il confezionamento l'applicazione in un file EXE e il lancio, ottengo WindowsError 5 o 6 (sembra casuale) a questa riga:

command = queue.get_nowait() 

Una ricetta alla homepage di PyInstaller afferma che Devo modificare il mio codice per abilitare la multielaborazione in Windows quando impacchetta l'applicazione come un singolo file.

sto riproducendo il codice qui:

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' 

La mia frustrazione con questa "soluzione" è che, uno, è assolutamente chiaro che cosa esattamente si patch, e, due, che è scritto in un tale modo complicato che diventa impossibile dedurre quali parti sono la soluzione e che sono solo un'illustrazione.

Qualcuno può condividere un po 'di luce su questo problema e fornire informazioni su cosa esattamente deve essere modificato in un progetto che consente la multiprocessing in file eseguibili di Windows a file singolo creati con PyInstaller?

+0

Does la ricetta risolto il problema? – dano

+0

Beh, non è chiaro (almeno per me) come applicare la ricetta. Anche incollare il codice precedente nel mio script Python principale non funziona, poiché solleva altre due eccezioni non correlate ai miei script Python. Il che mi dice che la ricetta è fondamentalmente difettosa. – nikola

+0

Se si esegue la ricetta solo come script autonomo, viene eseguita senza errori? – dano

risposta

6

rispondere alle mie domande proprio dopo aver trovato this PyInstaller ticket:

A quanto pare tutto ciò che dobbiamo fare è fornire una classe Process (e _Popen) come mostrato di seguito, e usarlo invece di multiprocessing.Process. Ho corretto e semplificato la classe affinché funzioni solo su Windows, * i sistemi ix potrebbero richiedere un codice diverso.

Per ragioni di completezza, ecco il campione adattato dalla domanda di cui sopra:

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 
+1

Il link del biglietto non è più valido. I documenti attuali per questo sono qui: https://github.com/pyinstaller/pyinstaller/wiki/Recipe-Multiprocessing – tom10

10

Per aggiungere alla risposta di Nikola ...

* nix (Linux, Mac OS X, etc.) NON richiede alcuna modifica affinché PyInstaller funzioni. (Questo include entrambe le opzioni --onedir e --onefile). Se si intende supportare solo sistemi * nix, non è necessario preoccuparsi di nulla.

Tuttavia, se si prevede di supportare Windows, sarà necessario aggiungere del codice, a seconda dell'opzione scelta: --onedir o --onefile.

Se si prevede di utilizzare --onedir, tutto quello che sarà necessario aggiungere è una speciale chiamata di metodo:

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

Secondo la documentazione, questa chiamata deve essere effettuato subito dopo if __name__ == '__main__':, altrimenti è destinato a non funziona. (Si consiglia vivamente di avere queste due linee nel modulo principale.)

In realtà, però, si può permettere di fare un controllo prima della chiamata, e le cose saranno ancora lavorare:

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

Tuttavia, chiamando multiprocessing.freeze_support() disponibile in altre piattaforme e situazioni, come pure - eseguirlo impatta solo il congelamento del supporto su Windows. Se sei un dado bytecode, noterai che l'istruzione if aggiunge qualche codice bytecode e consente di ottenere potenziali risparmi utilizzando un'istruzione if se trascurabile. Pertanto, è sufficiente attenersi a una semplice chiamata multiprocessing.freeze_support() immediatamente dopo if __name__ == '__main__':.

Se si prevede di utilizzare --onefile, è necessario aggiungere il codice di 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 

È possibile combinare il sopra con il resto del suo codice, o il seguente:

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' 

I ottenuto il codice da here, il nuovo sito di PyInstaller per la ricetta di multiprocessing. (Sembrano aver chiuso il loro sito basato su Trac.)

Si noti che hanno un errore minore con il loro codice per il supporto di multiprocessing --onefile. Aggiungono os.sep alla loro variabile di ambiente _MEIPASS2. (Linea: os.putenv('_MEIPASS2', sys._MEIPASS + os.sep)) Questo rompe le cose:

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

Il codice che ho fornito sopra è la stessa, senza la os.sep. La rimozione di os.sep risolve questo problema e consente l'elaborazione multiprocesso utilizzando la configurazione --onefile.

In sintesi:

Abilitazione del supporto --onedir multiprocessing su Windows (non funziona con --onefile su Windows, ma per il resto sicuro su tutte le piattaforme/configurazioni):

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

Abilitazione --onefile supporto multiprocessing su Windows (sicuro su tutte le piattaforme/configurazioni, compatibile 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 (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 

Fonti: PyInstaller Recipe, Python multiprocessing docs

+1

Grazie per la risposta dettagliata. Stavo avendo problemi con i thread di zombie che appaiono dopo aver chiuso la mia finestra Python principale (con tk) quando uso _-- opzione onefile_. L'ultimo frammento in cui ridefinisci Popen ha risolto il problema. Per chi lavora con Python> 3.4, è necessario utilizzare 'import multiprocessing.popen_spawn_win32 come forking' invece di' multiprocessing.forking'. –

+0

Non dimenticare che 'multiprocessing.freeze_support()' dovrebbe sempre essere la prima riga in '__name__ == '__main __'' e che non ci dovrebbe essere nessun altro codice eseguito prima di questa riga (cioè prima di '__name__ == '__main__ ''). Avevo alcune importazioni che eseguivano del codice, con il risultato che 'multiprocessing.freeze_support()' non aveva alcun effetto. – Guido