8.1. 協調的並行性

Asyncio のスケジューリングモデルは プリエンプティブ ではなく 協調的 です。この区別は、このセクションの残りの部分が構築されるうえで最も重要なメンタルモデルであるため、コードが登場する前に明確にしておく価値があります。

8.1.1. プリエンプティブ vs 協調的

プリエンプティブ スケジューラ — デスクトップオペレーティングシステムが多数のプログラムを同時に実行し続けるために使用するもの — は、現在実行中のコードを 任意の タイミングで一時停止し、別のコードに切り替えることができます。実行中のコードは特別なことを何もする必要はなく、スケジューラがそれを中断します。これによりプリエンプティブスケジューリングは非常に柔軟になります(どのコードも遅いことで他を枯渇させることができません)が、同時にそれは、切り替えがどこで起こるか分からない — 値を書き込んでいる途中や、リストを読み込んでいる途中ですら — ため、あらゆる共有変数を慎重に保護しなければならないことを意味します。

協調的 スケジューラは、現在実行中のコードが 明示的に制御を返す 地点でのみコード片の間を切り替えられます。asyncio では、それらの地点はすべての await と、内部的に制御を譲るコルーチンへのすべての呼び出し(最も一般的には asyncio.sleep())です。2 つの await の間では、実行中のコルーチンが CPU を独占します。

そこから 2 つの帰結が生じます:

  • await を一切行わないコルーチンは決して一時停止されません。 コルーチンが内部に await のないタイトループに留まると、スケジューラを独占し、他には何も進行しません。これを修正するには、ループ内の適切な地点で await asyncio.sleep_ms(0)(または他の何らかの待機呼び出し)を行います。

  • 共有状態は await の間では安全です。 2 つのコルーチンが、await を含まない操作の途中で交錯することはできません。プリエンプションが複数ステップの更新の途中に入り込んだときに生じる種類の破損 — あるコードが値を読み込んでいる間に、別のコードがそれを変更している途中である — は、ここでは単に起こり得ません。複数のコルーチンが await を またいで リソースを共有しなければならない場合には依然としてコルーチン間の調整が必要ですが、行の途中での交錯という問題は当てはまりません。

8.1.2. 3 つの層

あらゆる asyncio スクリプトは同じ 3 つの層から構築されます。次の 2 ページでそれらを詳しく説明します。これらは、それらを読む間に念頭に置いておくべきラベルです。

  • コルーチンasync def で宣言された関数で、それぞれが適切な場所で await を行う自己完結した作業単位です。Python の概要では async/await キーワードを紹介しました。asyncio では、それらはコルーチンがスケジューラに制御を返す手段です。

  • タスクasyncio.create_task() がコルーチンに対して施す、現在のものと並行してそれをスケジュールする ためのラッパーです。アプリケーションは通常、長時間実行されるジョブ(スナップショットループ、ネットワーククライアント、UART リーダーなど)のために少数のタスクを作成します。

  • イベントループ — その下にあるエンジンで、どのコルーチンが待機中でどれが実行可能かを追跡し、すべての await でタスク間を切り替えます。アプリケーションがループを記述するわけではなく、トップレベルのコルーチンを asyncio.run() に渡すと、ループがそこからすべてを駆動します。

アプリケーションをそのように — イベントループによって組み立てられた少数のコルーチンの集合として — 記述すると、並行性はプログラムの形状の性質となり、アプリケーションがステップごとに管理しなければならないものではなくなります。