web-dev-qa-db-ja.com

asyncioとマルチプロセッシングを組み合わせた場合、どのような問題が発生しますか(もしあれば)?

Pythonでスレッド化を初めて見たとき、ほとんどの人が気づいているように、実際に並列処理を行いたい、または少なくともチャンスを与えたい人にとって、人生を惨めにするGILがあります。

現在、Reactorパターンのようなものの実装を検討しています。事実上、あるスレッドのような着信ソケット接続をリッスンし、誰かが接続しようとすると、その接続を受け入れ、処理のために別のスレッドのようなものに渡します。

どのような負荷に直面しているのか(まだ)わかりません。現在、着信メッセージに2MBの上限が設定されていることを知っています。理論的には、毎秒数千を得ることができます(実際にそのようなものを見たかどうかはわかりませんが)。メッセージの処理に費やされる時間はそれほど重要ではありませんそれほど重要ではありません

私はReactorパターンを調査しており、multiprocessingライブラリーを使用して(少なくともテストでは)正常に動作するように見える小さな例を開発しました。ただし、今/間もなく asyncio ライブラリが使用可能になり、イベントループが処理されます。

asynciomultiprocessingを組み合わせることで私を噛むことができるものはありますか?

53
Wayne Werner

asynciomultiprocessingを問題なく組み合わせることができますが、multiprocessingを直接使用することはできません。 asyncio(およびその他のイベントループベースの非同期フレームワーク)の基本的な罪は、イベントループをブロックしています。 multiprocessingを直接使用しようとすると、子プロセスを待つためにブロックするたびに、イベントループがブロックされます。明らかに、これは悪いことです。

これを回避する最も簡単な方法は、 BaseEventLoop.run_in_executor を使用して concurrent.futures.ProcessPoolExecutor で関数を実行することです。 ProcessPoolExecutormultiprocessing.Processを使用して実装されるプロセスプールですが、asyncioにはイベントループをブロックせずに関数を実行するためのサポートが組み込まれています。以下に簡単な例を示します。

import time
import asyncio
from concurrent.futures import ProcessPoolExecutor

def blocking_func(x):
   time.sleep(x) # Pretend this is expensive calculations
   return x * 5

@asyncio.coroutine
def main():
    #pool = multiprocessing.Pool()
    #out = pool.apply(blocking_func, args=(10,)) # This blocks the event loop.
    executor = ProcessPoolExecutor()
    out = yield from loop.run_in_executor(executor, blocking_func, 10)  # This does not
    print(out)

if __== "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

ほとんどの場合、これは機能だけで十分です。 multiprocessingQueueEventなど、Managerからの他のコンストラクトが必要な場合、サードパーティライブラリと呼ばれる- aioprocessing (完全開示:私が書いた)、すべてのasyncioデータ構造のmultiprocessing互換バージョンを提供します。以下にデモの例を示します。

import time
import asyncio
import aioprocessing
import multiprocessing

def func(queue, event, lock, items):
    with lock:
        event.set()
        for item in items:
            time.sleep(3)
            queue.put(item+5)
    queue.close()

@asyncio.coroutine
def example(queue, event, lock):
    l = [1,2,3,4,5]
    p = aioprocessing.AioProcess(target=func, args=(queue, event, lock, l)) 
    p.start()
    while True:
        result = yield from queue.coro_get()
        if result is None:
            break
        print("Got result {}".format(result))
    yield from p.coro_join()

@asyncio.coroutine
def example2(queue, event, lock):
    yield from event.coro_wait()
    with (yield from lock):
        yield from queue.coro_put(78)
        yield from queue.coro_put(None) # Shut down the worker

if __== "__main__":
    loop = asyncio.get_event_loop()
    queue = aioprocessing.AioQueue()
    lock = aioprocessing.AioLock()
    event = aioprocessing.AioEvent()
    tasks = [ 
        asyncio.async(example(queue, event, lock)),
        asyncio.async(example2(queue, event, lock)),
    ]   
    loop.run_until_complete(asyncio.wait(tasks))
    loop.close()
62
dano

はい、噛む可能性がある(またはしない可能性のある)ビットがかなりあります。

  • asyncioのようなものを実行すると、1つのスレッドまたはプロセスで実行されることが期待されます。これは(単独では)並列処理では機能しません。 IO操作(特にソケットの操作)を単一のスレッド/プロセスに残したまま、何らかの方法で作業を分散する必要があります。
  • 個々の接続を別のハンドラープロセスに引き渡すというアイデアはすてきですが、実装するのは困難です。最初の障害は、接続を閉じずにasyncioから接続を引き出す方法が必要なことです。次の障害は、C拡張からプラットフォーム固有の(おそらくLinuxの)コードを使用しない限り、ファイル記述子を別のプロセスに単純に送信できないことです。
  • multiprocessingモジュールは、通信用の多数のスレッドを作成することが知られていることに注意してください。ほとんどの場合、通信構造(Queuesなど)を使用すると、スレッドが生成されます。残念ながら、これらのスレッドは完全には見えません。たとえば、(プログラムを終了する場合)クリーンダウンに失敗する可能性がありますが、それらの数によっては、リソースの使用量が単独で顕著になる場合があります。

個々のプロセスで個々の接続を本当に処理する場合は、さまざまなアプローチを検討することをお勧めします。たとえば、ソケットをリッスンモードにして、複数のワーカープロセスからの接続を同時に並行して受け入れることができます。ワーカーがリクエストの処理を完了すると、次の接続を受け入れることができるため、接続ごとにプロセスをフォークするよりも少ないリソースを使用できます。 SpamassassinとApache(mpm prefork)は、たとえばこのワーカーモデルを使用できます。ユースケースによっては、より簡単で堅牢になる場合があります。具体的には、構成された数の要求を処理した後にワーカーを停止させ、マスタープロセスによって再生成することにより、メモリリークの悪影響の多くを排除できます。

5
Helmut Grohne

PEP 3156、特にスレッドの相互作用に関するセクションを参照してください。

http://www.python.org/dev/peps/pep-3156/#thread-interaction

これは、run_in_executor()を含む、使用する可能性のある新しいasyncioメソッドを明確に文書化します。 Executorは、concurrent.futuresで定義されていることに注意してください。こちらもご覧になることをお勧めします。

1
Glenn