同時に実行される2つの Task
オブジェクトをPython 3の比較的新しい asyncio
モジュールを使用して適切に理解および実装しようとしています。
一言で言えば、asyncioは非同期プロセスと、イベントループでのTask
の同時実行を処理するように設計されているようです。イベントループをブロックせずに、結果を待機して使用するコールバックフリーの方法として、await
(非同期関数に適用)の使用を促進します。 (未来とコールバックはまだ実行可能な選択肢です。)
また、コルーチンをラップするように設計されたFuture
の特殊なサブクラスであるasyncio.Task()
クラスも提供します。 asyncio.ensure_future()
メソッドを使用して呼び出すことをお勧めします。 asyncioタスクの使用目的は、独立して実行するタスクを、同じイベントループ内の他のタスクと「同時に」実行できるようにすることです。私の理解では、Tasks
はイベントループに接続され、await
ステートメント間のコルーチンを自動的に駆動し続けます。
Executor
クラスのいずれかを使用せずに並行タスクを使用できるというアイデアが気に入っていますが、実装について詳しく説明していません。
これは私が現在それをやっている方法です:
import asyncio
print('running async test')
async def say_boo():
i = 0
while True:
await asyncio.sleep(0)
print('...boo {0}'.format(i))
i += 1
async def say_baa():
i = 0
while True:
await asyncio.sleep(0)
print('...baa {0}'.format(i))
i += 1
# wrap in Task object
# -> automatically attaches to event loop and executes
boo = asyncio.ensure_future(say_boo())
baa = asyncio.ensure_future(say_baa())
loop = asyncio.get_event_loop()
loop.run_forever()
2つのループタスクを同時に実行しようとすると、タスクに内部await
式がない限り、タスクがwhile
ループでスタックし、他のタスクの実行を事実上ブロックします(通常のwhile
ループのように)。ただし、タスクは(a)待機する必要があるとすぐに、問題なく並行して実行されるようです。
したがって、await
ステートメントは、タスク間で前後に切り替えるための足場をイベントループに提供し、同時実行の効果を提供しているように見えます。
内部await
を使用した出力例:
running async test
...boo 0
...baa 0
...boo 1
...baa 1
...boo 2
...baa 2
出力例withoutinternal await
:
...boo 0
...boo 1
...boo 2
...boo 3
...boo 4
この実装は、asyncio
の同時ループタスクの「適切な」例に合格していますか?
これが機能する唯一の方法は、Task
がブロッキングポイント(await
式)を提供して、イベントループが複数のタスクをジャグリングすることであるということは正しいですか?
はい。イベントループ内で実行されているコルーチンは、他のコルーチンとタスクの実行をブロックします。
yield from
またはawait
を使用して別のコルーチンを呼び出します(Python 3.5+を使用している場合)。これは、asyncio
がシングルスレッドであるためです。イベントループを実行する唯一の方法は、他のコルーチンがアクティブに実行されないようにすることです。 yield from
/await
を使用すると、コルーチンが一時的に中断され、イベントループが機能する機会が与えられます。
サンプルコードは問題ありませんが、多くの場合、最初からイベントループ内で非同期I/Oを実行していない長時間実行されるコードは望ましくないでしょう。このような場合、 BaseEventLoop.run_in_executor
を使用してバックグラウンドスレッドまたはプロセスでコードを実行する方が適切な場合がよくあります。タスクがCPUバウンドの場合はProcessPoolExecutor
の方が適しています。ThreadPoolExecutor
に対応していないI/Oを行う必要がある場合は、asyncio
が使用されます。
たとえば、2つのループは完全にCPUバウンドであり、状態を共有しないため、ProcessPoolExecutor
を使用してCPU間で各ループを並列に実行すると、最高のパフォーマンスが得られます。
import asyncio
from concurrent.futures import ProcessPoolExecutor
print('running async test')
def say_boo():
i = 0
while True:
print('...boo {0}'.format(i))
i += 1
def say_baa():
i = 0
while True:
print('...baa {0}'.format(i))
i += 1
if __== "__main__":
executor = ProcessPoolExecutor(2)
loop = asyncio.get_event_loop()
boo = asyncio.ensure_future(loop.run_in_executor(executor, say_boo))
baa = asyncio.ensure_future(loop.run_in_executor(executor, say_baa))
loop.run_forever()
イベントループを制御するためにyield from x
は必ずしも必要ではありません。
あなたの例では、proper方法はyield None
または同等のyield
ではなく、 yield from asyncio.sleep(0.001)
:
import asyncio
@asyncio.coroutine
def say_boo():
i = 0
while True:
yield None
print("...boo {0}".format(i))
i += 1
@asyncio.coroutine
def say_baa():
i = 0
while True:
yield
print("...baa {0}".format(i))
i += 1
boo_task = asyncio.async(say_boo())
baa_task = asyncio.async(say_baa())
loop = asyncio.get_event_loop()
loop.run_forever()
コルーチンは、単なる古いPythonジェネレーターです。内部的に、asyncio
イベントループはこれらのジェネレーターの記録を保持し、終了しないループでそれらのそれぞれに対してgen.send()
を1つずつ呼び出します。 yield
を呼び出すたびに、gen.send()
の呼び出しが完了し、ループを続行できます。 (私はそれを単純化しています; https://hg.python.org/cpython/file/3.4/Lib/asyncio/tasks.py#l265 を実際のコードについて見てみましょう)
ただし、データを共有せずにCPUを集中的に使用する計算を行う必要がある場合は、run_in_executor
ルートを使用します。