36. コルーチンと非同期
#36. コルーチンと非同期
コルーチンは、非同期プログラミングに使用される再開可能な計算です。 Python コードを一時停止させます。awaitポイントに戻り、制御をイベント ループに戻し、後で待機中の操作の結果が得られたときに再開します。
コルーチンはジェネレーターと似ており、どちらも一時停止後も実行状態を保持します。違いはプロトコルと目的です。
ジェネレーターはイテレーター・コンシューマーに値を生成します。
コルーチンは他の非同期操作を待機し、最終的に 1 つの最終結果を返します。python id="o2u8bi" async def fetch(): data = await read() return data 電話をかけるfetch()本体を最後まで実行しません。コルーチンオブジェクトを作成します。```python id="9i5ai1"
coro = fetch()
## 36.1 コルーチン関数とコルーチン オブジェクト
アン`async def`ステートメントはコルーチン関数を作成します。
コルーチン関数を呼び出すと、コルーチン オブジェクトが作成されます。```python id="rt8ky9"
async def work():
return 42
coro = work()
```概念的には:```text id="q71j65"
work
coroutine function object
work()
coroutine object
suspended execution state
code object
frame or frame-like state
```これは通常の関数とは異なります。```python id="nv1t8j"
def work():
return 42
work()
```通常の関数呼び出しはすぐに実行され、戻り値が返されます。`42`。
非同期関数呼び出しは、後で駆動する必要があるコルーチン オブジェクトを返します。
##36.2`await`
`await`別の awaitable が完了するまで現在のコルーチンを一時停止します。```python id="wjue72"
async def outer():
value = await inner()
return value + 1
```式:```python id="pbwts5"
await inner()
```概念的にいくつかのことを行います。```text id="n2ycfi"
call inner()
obtain awaitable object
suspend current coroutine
let event loop drive awaitable
resume current coroutine with result
```待機中の操作が発生すると、待機中のコルーチンに例外が注入されます。```python id="yc56f3"
async def outer():
try:
value = await inner()
except ValueError:
value = 0
return value
```## 36.3 待っているもの`await`待機可能なオブジェクトに対して動作します。
一般的な待機項目には次のようなものがあります。```text id="o2vwvk"
coroutine objects
asyncio Task objects
asyncio Future objects
objects implementing __await__
```カスタム オブジェクトは次のように定義することで待機可能になります。`__await__`。```python id="f4sd9q"
class Immediate:
def __await__(self):
yield
return 42
```実際には、ほとんどのアプリケーション コードは、非同期フレームワークによって作成されたコルーチン、タスク、フューチャーを待機します。
## 36.4 コルーチンの状態
コルーチンは、ジェネレーターの状態と同様の状態をとることができます。```text id="qcur3i"
created
running
suspended
closed
```使用`inspect`:
```python id="84zov8"
import inspect
async def work():
await other()
coro = work()
print(inspect.getcoroutinestate(coro))
```一般的な状態は次のとおりです。```text id="h0yb3u"
CORO_CREATED
CORO_RUNNING
CORO_SUSPENDED
CORO_CLOSED
```コルーチンは、複数のコンシューマによって同時に待機することはできません。これは 1 回の実行を表します。
## 36.5 コルーチンフレーム
中断されたコルーチンは実行状態を維持します。
その状態には次のものが含まれます。```text id="o61wj7"
code object
instruction position
local variables
value stack
exception state
currently awaited object
running state
closed state
```例:```python id="1tow3u"
async def process():
data = await read()
return transform(data)
```で一時停止されている間、`await read()`、コルーチンは次のことを覚えておく必要があります。```text id="985lzm"
current frame
local variables
await target
next instruction after await
```いつ`read()`完了すると、コルーチンが再開され、戻り値が`data`。
## 36.6 コルーチンは同じフレームモデルを使用する
コルーチンは別個のインタープリターを使用しません。これらは同じ CPython 評価ループによって実行されます。
ループはコルーチンまで実行されます。```text id="p7w1wv"
awaits and suspends
returns
raises
is cancelled
```概念的には:```text id="xzr16h"
coroutine object
frame state
bytecode
stack
locals
event loop
resumes coroutine
evaluation loop runs frame
stops at await or completion
```イベントループはスケジューリングを行います。 CPython は、再開可能な実行機構を提供します。
## 36.7 イベントループ
イベント ループは非同期タスクを調整します。
で`asyncio`、イベント ループは以下を管理します。```text id="n7xdfh"
ready tasks
sleep timers
socket readiness
future completion
callbacks
cancellation
exception reporting
```例:```python id="0chlb7"
import asyncio
async def main():
await asyncio.sleep(1)
return 42
result = asyncio.run(main())
print(result)
asyncio.runまでイベント ループを作成して駆動します。main()完了します。
CPython 自体はコルーチン オブジェクトを提供し、awaitセマンティクス。asyncio1 つの標準的なイベント ループ実装を提供します。
36.8 タスク
コルーチン オブジェクトだけはパッシブです。タスクは実行をスケジュールします。```python id="8et1tq" import asyncio
async def work(): await asyncio.sleep(1) return 42
async def main():
task = asyncio.create_task(work())
result = await task
return result
概念的には:text id="p93t67"
coroutine object
passive resumable computation
task event-loop managed wrapper resumes coroutine stores result or exception supports cancellation
## 36.9 協調的な同時実行性
Async Python は協調的な同時実行性を使用します。
コルーチンは、待機ポイントに到達するまで実行されます。```python id="8ux1ur"
async def work():
step1()
await wait()
step2()
```その間`step1()`、そうでない限り、そのイベント ループでは他のタスクは実行されません。`step1()`それ自体が待機するか制御を返します。
で`await wait()`、コルーチンは一時停止し、イベント ループは別のタスクを実行できます。
これは、コルーチン内の CPU 負荷の高いコードがイベント ループをブロックする可能性があることを意味します。```python id="g8fwp9"
async def bad():
while True:
compute()
```それなし`await`、このコルーチンは制御を与えません。
## 36.10 非同期は並列処理ではありません
非同期同時実行は、自動的に CPU 並列処理を意味するわけではありません。
通常、1 つのイベント ループは 1 つの OS スレッドで実行されます。一時停止ポイントでのみタスク間を切り替えます。
非同期は次の場合に適しています。```text id="oell4m"
network I/O
many open connections
timeouts
sleeping
waiting for subprocesses
streaming protocols
cooperative task orchestration
```CPU に依存する Python バイトコードを並列実行することはありません。
CPU に依存した作業の場合は、以下を使用します。```text id="r5575k"
native extensions
multiprocessing
process pools
thread pools for blocking calls
free-threaded builds where appropriate
external workers
```## 36.11 コルーチンを待つ
与えられる:```python id="h5auqh"
async def inner():
return 10
async def outer():
x = await inner()
return x + 1
```流れは次のとおりです。```text id="fi924o"
outer starts
outer calls inner(), producing inner coroutine
outer awaits inner coroutine
event loop drives inner
inner returns 10
outer resumes with x = 10
outer returns 11
```戻り値は待機境界を越えて一時停止されたフレームに戻ります。
## 36.12 未来を待つ
未来は、まだ存在しない可能性のある結果を表します。```python id="6xf2w8"
future = loop.create_future()
```コルーチンはそれを待つことができます。```python id="qze0h0"
value = await future
```Future が完了していない場合、コルーチンは一時停止します。
後で:```python id="nrkg2x"
future.set_result(42)
```イベント ループは、待機中のタスクに準備完了のマークを付けます。待機中のコルーチンは次のように再開されます`42`。
将来に例外がある場合:```python id="xzr05h"
future.set_exception(ValueError("bad"))
```待機ポイントで例外を発生させることにより、待機中のコルーチンが再開されます。
## 36.13 タスクを待っています
タスクも待機可能です。```python id="w7s6x8"
task = asyncio.create_task(work())
result = await task
```タスクが正常に終了すると、`await task`その結果を返します。
タスクが上がると、`await task`同じ例外が発生します。
タスクがキャンセルされた場合、`await task`上げる`CancelledError`。
##36.14 キャンセル
キャンセルはタスクの停止を要求します。```python id="j5ce0q"
task.cancel()
```で`asyncio`、キャンセルは注入によって配信されます`CancelledError`await ポイントでコルーチンに追加します。```python id="lczbrw"
async def work():
try:
await asyncio.sleep(10)
except asyncio.CancelledError:
cleanup()
raise
```コルーチンはクリーンアップのためにキャンセルをキャッチする場合がありますが、意図的にキャンセルを抑制しない限り、通常はそれを再発生させる必要があります。
キャンセルは協力的です。決して待機しないコルーチンでは、キャンセルがすぐに行われない可能性があります。
## 36.15 によるクリーンアップ`try/finally`使用`try/finally`非同期クリーンアップ用。```python id="j83mx6"
async def work():
resource = await acquire()
try:
await use(resource)
finally:
await release(resource)
```の`finally`ブロックは以下で実行されます:```text id="m2ocv7"
normal completion
exception
cancellation
```クリーンアップ自体が待機している場合、コルーチンはクリーンアップ中に一時停止する可能性があります。
これにより、キャンセルとクリーンアップが微妙になります。コルーチンは、終了コードの実行中にキャンセルされた状態になる可能性があります。
## 36.16 非同期コンテキストマネージャー
非同期コンテキストマネージャーが使用する`async with`。```python id="v2e2wy"
async with lock:
await work()
```オブジェクトは以下を提供する必要があります:```text id="5scc5n"
__aenter__
__aexit__
```概念的には:```python id="dkm7oc"
mgr = lock
value = await mgr.__aenter__()
try:
await work()
except BaseException as exc:
suppress = await mgr.__aexit__(type(exc), exc, exc.__traceback__)
if not suppress:
raise
else:
await mgr.__aexit__(None, None, None)
```非同期コンテキスト マネージャーを使用すると、リソースの取得と解放を一時停止できます。
## 36.17 非同期反復子
非同期反復の使用`async for`。```python id="hvh27p"
async for item in stream:
process(item)
```オブジェクトは以下を提供する必要があります:```text id="u37g80"
__aiter__
__anext__
```概念的には:```python id="z1gd9g"
ait = stream.__aiter__()
while True:
try:
item = await ait.__anext__()
except StopAsyncIteration:
break
process(item)
```非同期反復は、次の各項目が I/O を必要とする可能性があるストリームに役立ちます。
## 36.18 非同期ジェネレーター
非同期ジェネレーターは次のように定義されます。`async def`そして`yield`。```python id="65vpyy"
async def lines(reader):
async for line in reader:
yield line.strip()
```非同期イテレータを生成します。
使用:```python id="5lhj9g"
async for line in lines(reader):
print(line)
```非同期ジェネレーターは待機することも譲歩することもできます。```python id="8c9u5e"
async def gen():
await asyncio.sleep(1)
yield 1
```コルーチン サスペンションとジェネレーター スタイルの価値生成を組み合わせています。
##36.19`anext`
`anext`非同期イテレータから次の項目を取得します。```python id="vs0dl0"
item = await anext(ait)
```これが待っています`ait.__anext__()`。
非同期イテレータが使い果たされた場合は、`StopAsyncIteration`。
デフォルトを指定できます。```python id="8d97ug"
item = await anext(ait, default)
```## 36.20`StopAsyncIteration`非同期反復子は完了を通知します。`StopAsyncIteration`。
これは、`StopIteration`。```text id="prf78x"
normal iterator ends with StopIteration
async iterator ends with StopAsyncIteration
async forキャッチStopAsyncIterationループを終了します。
育てるStopIteration非同期反復からは正しくありません。
36.21 コルーチンのバイトコード
async 関数は、コルーチン コードとしてマークされたコード オブジェクトにコンパイルされます。```python id="m36blp" async def f(): return 1
await を使用した非同期関数:```python id="vb752s"
async def f():
value = await g()
return value
```概念的には、以下のバイトコードが含まれます。```text id="aw12fn"
call g
get awaitable iterator
send into awaitable
suspend if not done
resume with result
store value
return value
```正確なバイトコードは Python のバージョンによって異なります。モデルはそのままです:`await`再開可能なフレーム実行として実装されます。
##36.22`SEND`そして待つ
最新の CPython バイトコードは、送信のような機構を使用して待機可能ファイルを駆動します。
概念的には:```text id="c07p30"
await awaitable:
get awaitable iterator
send None or value
if it yields:
suspend current coroutine
if it returns:
resume with result
if it raises:
propagate exception
```これはジェネレーターの委任に関連しています。コルーチンは、一時停止された計算に値を送信するという同じ考え方に基づいて構築されています。
## 36.23 コルーチンの戻り
コルーチンは最終的な値を 1 つ返します。```python id="11vkrn"
async def f():
return 42
```待機中:```python id="q0irxd"
value = await f()
```待機中のコルーチンが受け取ります`42`。
内部的には、完了は awaitable プロトコルを通じて表されます。ユーザーレベルでは、`await`コルーチンの補完を通常の式の結果に変換します。
## 36.24 コルーチンの例外
コルーチンが発生する場合、待機中は発生します。```python id="ufon8f"
async def f():
raise ValueError("bad")
async def main():
await f()
```例外は次のように伝播します。`main`待機場所で。
あなたはそれを捕まえることができます:```python id="vi414s"
async def main():
try:
await f()
except ValueError:
return 0
```トレースバックには、非同期呼び出しチェーンに関与するコルーチン フレームが含まれます。
## 36.25 コルーチンの警告
コルーチンを作成してもそれを待たない場合は、通常はバグです。```python id="j9rvzc"
async def f():
return 1
f()
```これにより、コルーチン オブジェクトが作成され、それが破棄されます。
Python は次のように警告する場合があります。```text id="e1rugz"
RuntimeWarning: coroutine was never awaited
```体は決して走らなかった。
正しい使い方:```python id="5e8f4p"
await f()
```または:```python id="daxsm1"
asyncio.create_task(f())
```## 36.26`asyncio.run`
`asyncio.run`は、非同期メイン関数を実行するための標準のエントリ ポイントです。```python id="45lxp3"
import asyncio
async def main():
await asyncio.sleep(1)
return 42
print(asyncio.run(main()))
```イベント ループを作成し、コルーチンを最後まで実行し、最終的な非同期ジェネレーターのクリーンアップを処理して、ループを閉じます。
通常は、プログラムのトップレベルで 1 回呼び出します。
## 36.27 非同期コード内の呼び出しのブロック
呼び出しをブロックすると、イベント ループが停止します。
悪い:```python id="upggpj"
import time
async def main():
time.sleep(5)
```その間`time.sleep`、イベント ループは他のタスクを実行できません。
より良い:```python id="j76gyt"
import asyncio
async def main():
await asyncio.sleep(5)
```非同期にできない関数をブロックするには、エグゼキューターまたはスレッド ヘルパーを使用します。```python id="bo70ck"
result = await asyncio.to_thread(blocking_function, arg)
```これにより、イベント ループの応答性が維持されます。
## 36.28 タスクグループ
構造化された同時実行グループ関連のタスク。```python id="jrw68m"
import asyncio
async def main():
async with asyncio.TaskGroup() as tg:
tg.create_task(fetch_one())
tg.create_task(fetch_two())
```タスク グループはその子タスクを待ちます。 1 つのタスクが失敗した場合、グループはキャンセルを調整し、必要に応じてグループ化された例外を生成します。
これは、タスクを追跡せずにタスクを作成するより安全です。
## 36.29 非同期コードの例外グループ
同時実行タスクは同時に失敗する可能性があります。
タスク グループは、例外グループを使用して複数の失敗を報告できます。```python id="75j6zk"
try:
async with asyncio.TaskGroup() as tg:
tg.create_task(a())
tg.create_task(b())
except* ValueError as group:
handle_value_errors(group)
except*グループ化された例外をタイプごとに分割できます。
同じ構造化された操作中に独立したタスクが失敗する可能性があるため、これは非同期システムにとって重要です。
36.30 非同期ジェネレーターとクリーンアップ
非同期ジェネレーターには非同期クリーンアップが必要です。python id="6lt8y0" async def gen(): try: yield 1 finally: await cleanup() 非同期ジェネレーターが使い果たされていない場合は、次のようにして閉じる必要があります。aclose()。```python id="hupibv"
agen = gen()
item = await anext(agen)
await agen.aclose()
## 36.31 非同期ジェネレーターのメソッド
非同期ジェネレーターは、ジェネレーターと同様のメソッドをサポートしますが、待機可能です。
|方法 |意味 |
|---|---|
|`__anext__()`|次の項目を非同期で取得する |
|`asend(value)`|値を非同期ジェネレーターに送信する |
|`athrow(exc)`|非同期ジェネレーターに例外をスローする |
|`aclose()`|非同期ジェネレーターを閉じる |
これらのメソッドは待機可能オブジェクトを返します。
例:```python id="iuoltd"
value = await agen.__anext__()
await agen.aclose()
```## 36.32 コルーチンと参照の有効期間
中断されたコルーチンはローカル変数を維持します。```python id="3yoa52"
async def work():
data = bytearray(100_000_000)
await asyncio.sleep(10)
return len(data)
```寝ている間、`data`コルーチンが再開して使用できるため、このファイルは生きたままになります。
保持チェーン:```text id="fhhddc"
task
coroutine
suspended frame
local data
```可能であれば、長い間待たされる前に大きなローカルをクリアします。```python id="7hxthq"
async def work():
data = bytearray(100_000_000)
result = process(data)
data = None
await asyncio.sleep(10)
return result
```## 36.33 タスクの存続期間
タスクはそのコルーチンを存続させます。```python id="4ffgq1"
task = asyncio.create_task(work())
```タスクが完了するか、キャンセルされて終了するまで、タスクはコルーチンとそのフレーム状態を参照します。
タスクを作成して追跡できなくなると、後で例外がログに記録され、リソースが予想より長くアクティブなままになる可能性があります。
タスク グループなどの構造化されたパターンは、ライフタイムの制御に役立ちます。
## 36.34 非同期スタックトレース
非同期スタック トレースには、中断されたコルーチン チェーンが含まれます。
例外を越えたとき`await`トレースバックは、それが発生した場所と待機されていた場所を示します。
例:```python id="uyj5ih"
async def a():
await b()
async def b():
await c()
async def c():
1 / 0
```トレースバックは、次の非同期チェーンを表示できます。`a`に`b`に`c`。
これが可能になるのは、コルーチン フレームがコード オブジェクトと命令の位置情報を保持するためです。
## 36.35`contextvars`非同期コードでよく使用されるのは、`contextvars`コンテキストローカル状態の場合。```python id="ulzcon"
from contextvars import ContextVar
request_id = ContextVar("request_id")
async def handle():
print(request_id.get())
```スレッドローカル ストレージとは異なり、コンテキスト変数は非同期タスクの切り替え間で正しく動作するように設計されています。
各タスクは独自のコンテキストを保持できます。
これは次の場合に重要です。```text id="ucdnv4"
request IDs
tracing
logging context
database transaction context
auth state
locale settings
```## 36.36 非同期と GIL
非同期コードでは GIL は削除されません。
従来の CPython では次のようになります。```text id="vow7l4"
one thread executes Python bytecode at a time per interpreter
async tasks switch cooperatively at await points
```ほとんどの時間は外部イベントの待機に費やされるため、非同期コードは多くの I/O バウンド タスクを効率的に処理できます。
CPU に負荷のかかる Python ループを並列実行することはありません。
## 36.37 非同期とスレッド
非同期とスレッドはさまざまな問題を解決します。
|特集 |非同期タスク |スレッド |
|---|---|---|
|スケジューリング |協同組合 | OS とインタープリタチェックによるプリエンプティブ |
|スイッチポイント |`await`|スレッドのスケジューリングと GIL ハンドオフ |
|最適 |多くの I/O タスク |ブロッキング API、同期コードとの統合 |
|共有メモリ |通常同じスレッド |スレッド間で共有 |
|人種のリスク |低いがまだ可能 |より高い |
|従来の CPython における CPU 並列処理 |いいえ | Python バイトコードの GIL による制限 |
非同期コードは一時停止について明示的です。スレッドは、わかりにくいポイントで切り替わる可能性があります。
## 36.38 非同期とジェネレーター
Coroutines and generators share resumable execution, but the protocols differ.
|特集 |ジェネレーター |コルーチン |
|---|---|---|
|定義 |`def`と`yield` | `async def`|
|プロデュース |複数の生成値 |最終的な結果は 1 つ |
|消費者 |`for`、`next`、`send` | `await`、イベントループ |
|完成 |`StopIteration`|結果または例外を待つ |
|主な用途 |遅延反復 |非同期 I/O 調整 |
|待つことができます |いいえ |はい |
|プレーンな値を生成できる |はい |いいえ、非同期ジェネレーターを除きます。
コルーチンはイテレータではありません。ジェネレーターは、適応されない限り、直接待機できません。
## 36.39 最小限のコルーチン スケジューラ
おもちゃのスケジューラは、核となるアイデアを示すことができます。```python id="zytwib"
from collections import deque
class Sleep:
def __await__(self):
yield self
return None
async def task(name):
print(name, "start")
await Sleep()
print(name, "end")
def run(coros):
ready = deque(c.__await__() for c in coros)
while ready:
aw = ready.popleft()
try:
next(aw)
except StopIteration:
continue
ready.append(aw)
run([task("a"), task("b")])
```これはそうではありません`asyncio`。実際の I/O、タイマー、先物、キャンセル、例外、タスクの状態は無視されます。しかし、重要なメカニズムが示されています。```text id="6z9pem"
coroutine runs
await yields control
scheduler later resumes it
```## 36.40 よくある誤解
|誤解 |正しいモデル |
|---|---|
|非同期関数を呼び出すと実行されます。コルーチン オブジェクトを作成します。
|`await`新しいスレッドを開始します | awaitable が完了するまで現在のコルーチンを一時停止します。
| Async makes CPU code parallel | It mainly helps cooperative I/O concurrency |
| A coroutine can be awaited repeatedly | A coroutine object represents one execution |
|`asyncio.create_task`と同じです`await`|同時にスケジュールを設定します。`await`完了を待ちます |
|呼び出しのブロックは非同期コード内で問題ありません。これらはイベント ループをブロックします。
|キャンセルするとコードが即座に強制終了されます。連携して例外を挿入します。
|非同期は別のインタープリタを使用します。 CPython フレームと評価ループを使用します。
## 36.41 読書戦略
以下から始めます:```python id="bltl80"
async def f():
return 1
```それから:```python id="zf4zrn"
async def g():
value = await f()
return value + 1
```検査:```python id="60r9ah"
import dis
import inspect
print(inspect.iscoroutinefunction(f))
coro = f()
print(inspect.getcoroutinestate(coro))
dis.dis(g)
```次に、次のことを勉強します。```text id="gpxkbm"
await
async with
async for
async generators
tasks
cancellation
TaskGroup
contextvars
blocking calls inside async code
```ケースごとに、以下を追跡します。```text id="xhs827"
when the coroutine object is created
when the body starts
where it suspends
what object it awaits
how it resumes
what happens on exception or cancellation
when its frame is cleared
```## 36.42 章の概要
コルーチンは、非同期プログラミングに使用される再開可能な計算です。アン`async def`call はコルーチン オブジェクトを作成します。コルーチン本体は、待機中またはスケジュールされた場合にのみ実行されます。それぞれに`await`を使用すると、コルーチンはフレーム状態を保持して一時停止し、後で値または例外を使用して再開できます。
コアモデルは次のとおりです。```text id="r29f1z"
async function call
↓
create coroutine object
↓
event loop or await resumes coroutine
↓
run until await, return, raise, or cancellation
↓
await suspends and yields control
↓
resume later with result or exception
```非同期実行は、通常の実行と同じ CPython 基盤 (コード オブジェクト、フレーム、バイトコード、例外、オブジェクト プロトコル) に基づいて構築されます。イベント ループはスケジューリングを提供します。 CPython は再開可能な実行を提供します。