web-dev-qa-db-ja.com

マルチプロセッシングプールのapply_asyncメソッドを使用する場合、誰がコールバックを実行しますか?

マルチプロセッシングプールのapply_syncメソッドを使用するときに、舞台裏で何が起こっているのかを少し理解しようとしています。

コールバックメソッドを実行するのは誰ですか? apply_asyncを呼び出したのはメインプロセスですか?

コールバック付きの一連のapply_asyncコマンドを送信して、プログラムを続行するとします。私のプログラムは、apply_asyncの最初から最後までまだ動作しています。メインプロセスがスクリプトでビジー状態のときに、コールバックはどのようにして「メインプロセス」を実行しますか?

ここに例があります。

import multiprocessing
import time

def callback(x):
    print '{} running callback with arg {}'.format(multiprocessing.current_process().name, x)

def func(x):
    print '{} running func with arg {}'.format(multiprocessing.current_process().name, x)
    return x

pool = multiprocessing.Pool()

args = range(20)

for a in args:
    pool.apply_async(func, (a,), callback=callback)

print '{} going to sleep for a minute'.format(multiprocessing.current_process().name)

t0 = time.time()
while time.time() - t0 < 60:
    pass

print 'Finished with the script'

出力は次のようなものです

Arg 0でfuncを実行しているPoolWorker-1

Arg 1でfuncを実行するPoolWorker-2

Arg 2でfuncを実行するPoolWorker-3

MainProcessは1分間スリープします<-メインプロセスはビジーです

Arg 3でfuncを実行するPoolWorker-4

Arg 4でfuncを実行するPoolWorker-1

Arg 5でfuncを実行するPoolWorker-2

Arg 6でfuncを実行するPoolWorker-3

Arg 7でfuncを実行するPoolWorker-4

Arg 0でMainProcess実行中のコールバック<-メインプロセスが実行中のコールバックがwhileループ中にある!!

引数1のMainProcess実行コールバック

引数2のMainProcess実行コールバック

引数3のMainProcess実行コールバック

引数4でMainProcess実行コールバック

Arg 8でfuncを実行するPoolWorker-1

...

スクリプトで終了

MainProcessは、whileループの途中でコールバックをどのように実行していますか?

multiprocessing.Pool のドキュメントのコールバックに関するこのステートメントはヒントのように見えますが、理解できません。

apply_async(func [、args [、kwds [、callback]]])

結果オブジェクトを返すapply()メソッドのバリアント。

コールバックが指定されている場合、単一の引数を受け入れる呼び出し可能オブジェクトである必要があります。結果が準備完了になると、コールバックが適用されます(呼び出しが失敗した場合を除く)。そうしないと、結果を処理するスレッドがブロックされるため、コールバックはすぐに完了する必要があります。

37
Alex

確かにドキュメントにヒントがあります:

それ以外の場合は結果を処理するスレッドがブロックされるため、コールバックはすぐに完了するはずです。

コールバックはメインプロセスで処理されますが、それらは独自の個別のスレッドで実行されますPoolを作成すると、実際にはいくつかのThreadオブジェクトが内部的に作成されます。

class Pool(object):
    Process = Process

    def __init__(self, processes=None, initializer=None, initargs=(),
                 maxtasksperchild=None):
        self._setup_queues()
        self._taskqueue = Queue.Queue()
        self._cache = {}
        ... # stuff we don't care about
        self._worker_handler = threading.Thread(
            target=Pool._handle_workers,
            args=(self, )
            )
        self._worker_handler.daemon = True
        self._worker_handler._state = RUN 
        self._worker_handler.start()

        self._task_handler = threading.Thread(
            target=Pool._handle_tasks,
            args=(self._taskqueue, self._quick_put, self._outqueue,
                  self._pool, self._cache)
            )
        self._task_handler.daemon = True
        self._task_handler._state = RUN 
        self._task_handler.start()

        self._result_handler = threading.Thread(
            target=Pool._handle_results,
            args=(self._outqueue, self._quick_get, self._cache)
            )
        self._result_handler.daemon = True
        self._result_handler._state = RUN
        self._result_handler.start()

私たちにとって興味深いスレッドは_result_handlerです。その理由については後ほど説明します。

ギアを1秒間切り替えて、apply_asyncを実行すると、ApplyResultオブジェクトが内部で作成され、子からの結果の取得が管理されます。

def apply_async(self, func, args=(), kwds={}, callback=None):
    assert self._state == RUN
    result = ApplyResult(self._cache, callback)
    self._taskqueue.put(([(result._job, None, func, args, kwds)], None))
    return result

class ApplyResult(object):

    def __init__(self, cache, callback):
        self._cond = threading.Condition(threading.Lock())
        self._job = job_counter.next()
        self._cache = cache
        self._ready = False
        self._callback = callback
        cache[self._job] = self


    def _set(self, i, obj):
        self._success, self._value = obj
        if self._callback and self._success:
            self._callback(self._value)
        self._cond.acquire()
        try:
            self._ready = True
            self._cond.notify()
        finally:
            self._cond.release()
        del self._cache[self._job]

ご覧のとおり、_setメソッドは、タスクが成功したと仮定して、渡されたcallbackを実際に実行するメソッドです。また、__init__の末尾のグローバルcache辞書に自分自身を追加することにも注意してください。

次に、_result_handlerスレッドオブジェクトに戻ります。そのオブジェクトは、次のような_handle_results関数を呼び出します。

    while 1:
        try:
            task = get()
        except (IOError, EOFError):
            debug('result handler got EOFError/IOError -- exiting')
            return

        if thread._state:
            assert thread._state == TERMINATE
            debug('result handler found thread._state=TERMINATE')
            break

        if task is None:
            debug('result handler got sentinel')
            break

        job, i, obj = task
        try:
            cache[job]._set(i, obj)  # Here is _set (and therefore our callback) being called!
        except KeyError:
            pass

        # More stuff

これは、子からの結果をキューから取り出し、cacheでそのエントリを見つけ、_setを呼び出してコールバックを実行するループです。メインスレッドで実行されていないため、ループ状態であっても実行できます。

36
dano