Pythonでスレッド化を初めて見たとき、ほとんどの人が気づいているように、実際に並列処理を行いたい、または少なくともチャンスを与えたい人にとって、人生を惨めにするGILがあります。
現在、Reactorパターンのようなものの実装を検討しています。事実上、あるスレッドのような着信ソケット接続をリッスンし、誰かが接続しようとすると、その接続を受け入れ、処理のために別のスレッドのようなものに渡します。
どのような負荷に直面しているのか(まだ)わかりません。現在、着信メッセージに2MBの上限が設定されていることを知っています。理論的には、毎秒数千を得ることができます(実際にそのようなものを見たかどうかはわかりませんが)。メッセージの処理に費やされる時間はそれほど重要ではありませんそれほど重要ではありません。
私はReactorパターンを調査しており、multiprocessing
ライブラリーを使用して(少なくともテストでは)正常に動作するように見える小さな例を開発しました。ただし、今/間もなく asyncio ライブラリが使用可能になり、イベントループが処理されます。
asyncio
とmultiprocessing
を組み合わせることで私を噛むことができるものはありますか?
asyncio
とmultiprocessing
を問題なく組み合わせることができますが、multiprocessing
を直接使用することはできません。 asyncio
(およびその他のイベントループベースの非同期フレームワーク)の基本的な罪は、イベントループをブロックしています。 multiprocessing
を直接使用しようとすると、子プロセスを待つためにブロックするたびに、イベントループがブロックされます。明らかに、これは悪いことです。
これを回避する最も簡単な方法は、 BaseEventLoop.run_in_executor
を使用して concurrent.futures.ProcessPoolExecutor
で関数を実行することです。 ProcessPoolExecutor
はmultiprocessing.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())
ほとんどの場合、これは機能だけで十分です。 multiprocessing
、Queue
、Event
など、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()
はい、噛む可能性がある(またはしない可能性のある)ビットがかなりあります。
asyncio
のようなものを実行すると、1つのスレッドまたはプロセスで実行されることが期待されます。これは(単独では)並列処理では機能しません。 IO操作(特にソケットの操作)を単一のスレッド/プロセスに残したまま、何らかの方法で作業を分散する必要があります。asyncio
から接続を引き出す方法が必要なことです。次の障害は、C拡張からプラットフォーム固有の(おそらくLinuxの)コードを使用しない限り、ファイル記述子を別のプロセスに単純に送信できないことです。multiprocessing
モジュールは、通信用の多数のスレッドを作成することが知られていることに注意してください。ほとんどの場合、通信構造(Queue
sなど)を使用すると、スレッドが生成されます。残念ながら、これらのスレッドは完全には見えません。たとえば、(プログラムを終了する場合)クリーンダウンに失敗する可能性がありますが、それらの数によっては、リソースの使用量が単独で顕著になる場合があります。個々のプロセスで個々の接続を本当に処理する場合は、さまざまなアプローチを検討することをお勧めします。たとえば、ソケットをリッスンモードにして、複数のワーカープロセスからの接続を同時に並行して受け入れることができます。ワーカーがリクエストの処理を完了すると、次の接続を受け入れることができるため、接続ごとにプロセスをフォークするよりも少ないリソースを使用できます。 SpamassassinとApache(mpm prefork)は、たとえばこのワーカーモデルを使用できます。ユースケースによっては、より簡単で堅牢になる場合があります。具体的には、構成された数の要求を処理した後にワーカーを停止させ、マスタープロセスによって再生成することにより、メモリリークの悪影響の多くを排除できます。
PEP 3156、特にスレッドの相互作用に関するセクションを参照してください。
http://www.python.org/dev/peps/pep-3156/#thread-interaction
これは、run_in_executor()を含む、使用する可能性のある新しいasyncioメソッドを明確に文書化します。 Executorは、concurrent.futuresで定義されていることに注意してください。こちらもご覧になることをお勧めします。