C++ 基礎からマスターまで — 第21回
Coroutine の中断、再開、寿命、キャンセルを理解する
本ブログの記事は、講義を目的として作成しています。 内容に誤りが含まれる可能性があるため、対象コンパイラと標準ライブラリの資料も確認してください。
Coroutine は関数を途中で中断し、後で同じ状態から再開できます。自動でスレッドや非同期 I/O を提供する機能ではなく、非同期制御フローを作る言語レベルのプロトコルです。
状態は Coroutine frame に保存される
co_await、co_yield、co_return を使うと、コンパイラはローカル変数と再開位置を保持する状態機械へ関数を変換します。
Task<Texture> LoadTextureAsync(Path path)
{
FileBytes bytes = co_await ReadFileAsync(path);
DecodedImage image = co_await DecodeAsync(std::move(bytes));
co_return UploadTexture(std::move(image));
}
例の Task と awaitable は、プロジェクトまたは非同期ライブラリが定義する契約です。標準 C++20 は Coroutine の変換規則を提供しますが、共通 executor やネットワーク event loop は提供しません。
co_await は中断方法を awaitable に問い合わせる
await_ready 中断せず結果を使えるか
await_suspend 中断後に誰が handle を保持し再開するか
await_resume 再開後にどの値またはエラーを返すか
処理済みなら中断せず継続できます。中断する場合は Coroutine handle を scheduler や I/O に安全に渡し、正確に一度だけ再開します。
Frame の所有権が寿命の中心
呼び出し側が返された Task をすぐ捨てた場合、処理は cancel、detach、または別の所有者へ移るのでしょうか。callback が破棄済み frame を再開すると use-after-free です。
参照で受け取ったオブジェクトも中断中に消える可能性があります。
Task<void> ShowLater(const Player& player)
{
co_await NextFrame();
Draw(player.Name); // player はまだ生きているか
}
必要な値を frame にコピーする、安定した handle を使う、または呼び出し側の寿命保証を明示します。
キャンセルとエラーもプロトコルです。待機中処理を起こし、クリーンアップを実行し、上位 Task に結果を伝えます。promise type が例外を co_await 位置で再送出するか、エラー値にするかを決めます。
Coroutine は順次コードのように読めても複数時点で動作します。frame 所有権、executor、再開スレッド、キャンセル、エラー伝播を文書化し、採用する Task 実装をテストします。