2014-07-24 13 views
10

W moim projekcie używam biblioteki Pythona multiprocessing do tworzenia wielu procesów w __main__. Projekt jest pakowany do pojedynczego pliku EXE systemu Windows przy użyciu PyInstaller 2.1.1.Program EXE systemu Windows skompilowany przez PyInstaller kończy się niepowodzeniem z wieloprocesowym przetwarzaniem

tworzę nowe procesy tak:

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 

I __main__:

if __name__ == '__main__': 
    freeze_support() 

    start() 

Niestety, podczas pakowania aplikacji do EXE i uruchomienia go, mam WindowsError 5 lub 6 (wydaje losowo) w tej linii:

command = queue.get_nowait() 

Przepis na stronie PyInstaller twierdzi, że Muszę zmodyfikować mój kod, aby włączyć obsługę wieloprocesową w systemie Windows podczas pakowania aplikacji jako pojedynczego pliku.

Jestem odtwarzając kod tutaj:

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' 

Moja frustracja z tego „rozwiązania” jest to, że jeden, to jest absolutnie jasne, co dokładnie jest łatanie, a dwa, że ​​jest napisane w taki zawiłym sposobem, że niemożliwe staje się ustalenie, które części są rozwiązaniem, a które są tylko ilustracją.

Czy ktoś może podzielić się trochę uwagą na ten temat i zapewnić wgląd w to, co dokładnie należy zmienić w projekcie, który umożliwia wieloprocesorowość w plikach wykonywalnych systemu Windows utworzonych przez PyInstaller?

+0

Czy przepis naprawia problem? – dano

+0

Cóż, nie jest jasne (przynajmniej dla mnie), jak zastosować przepis. Wklejenie powyższego kodu w moim głównym skrypcie Pythona również nie działa, ponieważ podnosi dwa dodatkowe wyjątki niezwiązane z moimi skryptami w Pythonie. Co mi mówi, że przepis jest zasadniczo wadliwy. – nikola

+0

Jeśli po prostu uruchomisz przepis jako samodzielny skrypt, działa on bezbłędnie? – dano

Odpowiedz

6

Odpowiadając na moje własne pytania po znalezieniu this PyInstaller ticket:

Podobno wszystko, co musimy zrobić, to zapewnić Process (i _Popen) klasę, jak pokazano poniżej, i używać go zamiast multiprocessing.Process. Poprawiłem i uprościłem klasę do pracy tylko w systemie Windows, * systemy ix mogą wymagać innego kodu.

Dla kompletności, tutaj jest przystosowany próbka z powyższym pytaniem:

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

Link do biletu nie jest już ważny. Aktualne dokumenty na ten temat są dostępne tutaj: https://github.com/pyinstaller/pyinstaller/wiki/Recipe-Multiprocessing – tom10

10

Aby dodać do odpowiedzi Nikola za ...

* nix (Linux, Mac OS X, itd.) NIE wymaga żadnych zmian dla PyInstaller do pracy. (Obejmuje to zarówno opcje --onedir, jak i --onefile.) Jeśli zamierzasz tylko obsługiwać systemy * nix, nie musisz się o to martwić.

Jeśli jednak planujesz obsługę systemu Windows, musisz dodać kod, w zależności od wybranej opcji: --onedir lub --onefile.

Jeśli planujesz używać --onedir, wszystko trzeba będzie dodać to specjalny wywołanie metody:

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

Zgodnie z dokumentacją, to wezwanie musi być wykonana natychmiast po if __name__ == '__main__':, albo to będzie nie działa. (Zaleca się, aby mieć te dwie linie w głównym module.)

W rzeczywistości jednak, można sobie pozwolić na sprawdzenie przed wywołaniem, a rzeczy będą nadal działać:

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

Jednak wywołanie multiprocessing.freeze_support() jest dostępny na innych platformach i sytuacje, a także - uruchomienie go wpływa tylko na funkcję blokowania w systemie Windows. Jeśli jesteś nakrętką kodu bajtowego, zauważysz, że instrukcja if dodaje kod bajtowy i sprawia, że ​​potencjalne oszczędności mogą być pomijane przy użyciu instrukcji if. Dlatego powinieneś po prostu trzymać się prostej rozmowy multiprocessing.freeze_support() natychmiast po if __name__ == '__main__':.

Jeśli planujesz używać --onefile, trzeba będzie dodać Nikola za kod:

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 

Można łączyć powyższe z resztą swojego kodu lub następuje:

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 otrzymałem kod z here, nowej witryny PyInstaller dla receptury wieloprocesowej. (Wydaje się, że zamknęli witrynę opartą na Tracku).

Należy zauważyć, że mają one niewielki błąd z kodem pod numerem --onefile w zakresie obsługi wieloprocesowej. Dodają os.sep do swojej zmiennej środowiskowej _MEIPASS2. (Line: os.putenv('_MEIPASS2', sys._MEIPASS + os.sep)) To łamie rzeczy:

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

Kod I podane powyżej są takie same, bez os.sep. Usunięcie os.sep rozwiązuje ten problem i umożliwia pracę wieloprocesorową przy użyciu konfiguracji --onefile.

Podsumowując:

Włączanie wsparcia --onedir Multiprocessing na Windows (nie działa z --onefile na Windows, ale w inny sposób bezpieczny na wszystkich platformach/konfiguracje):

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

Włączenie --onefile wsparcie wieloprocesorowe na Windows (bezpieczny na wszystkich platformach/konfiguracjach, kompatybilny z --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 

Źródła: PyInstaller Recipe, Python multiprocessing docs

+1

Dzięki za szczegółową odpowiedź. Miałem problemy z wątkami zombie pojawiającymi się po zamknięciu głównego okna Pythona (z tk), gdy używam opcji _-- onefile_. Ostatni fragment, w którym redefiniujesz Popena, naprawił problem. Dla każdego, kto pracuje z Pythonem> 3.4, należy użyć 'importu wieloprocesowego.popen_spawn_win32 jako rozwidlenia' zamiast' multiprocessing.forking'. –

+0

Nie zapominaj, że 'multiprocessing.freeze_support()' powinien zawsze być pierwszym wierszem w '__name__ == '__main __'' i że nie powinno być żadnego innego kodu wykonanego przed tą linią (tj. Przed '__name__ == '__main__ "). Miałem kilka importów, które wykonały jakiś kod, powodując, że 'multiprocessing.freeze_support()' nie ma żadnego efektu. – Guido

Powiązane problemy