web-dev-qa-db-ja.com

PyInstallerで構築されたWindowsEXEがマルチプロセッシングで失敗する

私のプロジェクトでは、Pythonのmultiprocessingライブラリを使用して、__ main__に複数のプロセスを作成しています。プロジェクトは、PyInstaller2.1.1を使用して単一のWindowsEXEにパッケージ化されています。

私は次のような新しいプロセスを作成します。

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'

この「解決策」に対する私の不満は、1つは、パッチが正確に何であるかが完全に不明であり、2つは、どの部分が解決策であり、どの部分が単なる図。

誰かがこの問題についていくつかの光を共有し、PyInstallerで構築された単一ファイルのWindows実行可能ファイルでマルチプロセッシングを可能にするプロジェクトで正確に何を変更する必要があるかについての洞察を提供できますか?

17
nikola

見つけた後、私自身の質問に答える このPyInstallerチケット

どうやら私たちがしなければならないのは、以下に示すように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
6
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__':_の後にすぐに行う必要があります。そうしないと機能しません。 (メインモジュールにこれらの2行を含めることを強くお勧めします。)

ただし、実際には、電話をかける前にチェックを行う余裕があり、それでも問題は解決します。

_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
_

Error when using os.sep in _MEIPASS2

上で提供したコードは同じですが、_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 RecipePythonマルチプロセッシングドキュメント

14
Albert H