8.4. Gather¶
前章では、asyncio.create_task() がコルーチンをスケジュールし、それを待たない(does not wait)様子を示しました。これに対応する操作 -- 複数のアウェイタブルを同時に実行し、そのすべてを待つ -- が asyncio.gather() です。
8.4.1. 基本的な形¶
import asyncio
async def fetch(name, delay_ms):
await asyncio.sleep_ms(delay_ms)
return name
async def main():
results = await asyncio.gather(
fetch("a", 100),
fetch("b", 200),
fetch("c", 50),
)
print(results)
asyncio.run(main())
出力:
['a', 'b', 'c']
注目すべき点が2つあります。1つ目は、結果のリストは gather への引数 の順序であって、コルーチンが終了した順序ではないことです -- "c" が最初に返ったにもかかわらず、依然として3番目のエントリです。2つ目は、呼び出し全体にかかった時間が 350 ms ではなく 200 ms だったことです。3つのスリープは同時に実行され、gather() は最も遅いものが完了するとすぐに返ります。
どちらの事実も同じ源から生じています。gather() は、まだタスクになっていない各引数(この例ではコルーチン)をラップしてループ上にスケジュールし、それら すべて が終了するまで呼び出し元のコルーチンを中断し、それから元の順序で結果を返します。
8.4.2. いつ使うか¶
アプリケーションが N 個のアウェイタブルな操作を持ち、続行する前にそのすべての結果を必要とする場面ならどこでも使えます。典型的な例は次のとおりです。
複数のネットワークリクエストを並行して発行し、すべての応答を待つ。
複数のセンサーから同時に読み取り、組み合わせた結果を処理する前に待つ。
アプリケーションのある段階の終わりに、複数の短命なヘルパータスクを合流させる。
アプリケーションが起動してプログラムの存続期間にわたって動かし続ける、長時間実行のバックグラウンドタスクには 適していません -- それらは依然として create_task() の仕事です。gather() は ファンアウト / ファンイン パターンのためのものです。作業を分割し、同時に実行し、再び合流させるのです。
8.4.3. グループ内での例外¶
gather されたアウェイタブルのいずれかが例外を送出すると、デフォルトの動作は その例外を gather 呼び出しの外へ再送出する ことです。まだ終了していない兄弟タスクは、バックグラウンドでキャンセルされます。
これは通常アプリケーションが望むことです -- N 個の並列ジョブのうち1つが失敗したのだから、組み合わせた操作は失敗したことになり、残りに時間を費やすのをやめるのです。アプリケーションが逆を望むこともあります。各アウェイタブルが独立して終了(または失敗)するに任せ、後で結果を調べたい場合です。そのためには return_exceptions=True を渡します:
results = await asyncio.gather(
fetch_or_fail("ok"),
fetch_or_fail("bad"),
return_exceptions=True,
)
# results == ["ok-value", OSError(...)]
返されるリストの各エントリは、今や通常の戻り値か、対応するアウェイタブルが送出した例外 のいずれかになります。呼び出し元は isinstance(r, Exception) をチェックして両者を見分けます。
8.4.4. キャンセル¶
gather 自体をキャンセルすること -- それを await していたタスクをキャンセルすることによって -- は、その内部でまだ実行中のすべてのアウェイタブルをキャンセルします。タイムアウトとキャンセル ページでは、キャンセルが呼び出しチェーンをどう伝播するかを詳しく取り上げています。