从 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 时,工作是取消、detach,还是由其他对象拥有?callback 恢复已销毁的 frame 会导致 use-after-free。
Coroutine 通过引用接收的对象也可能在挂起期间消失。
Task<void> ShowLater(const Player& player)
{
co_await NextFrame();
Draw(player.Name); // player 是否仍然存在?
}
可以把必要数据复制进 frame、使用稳定 handle,或明确要求调用者保证生命周期。
取消和错误也是协议。取消必须唤醒等待中的操作、运行清理并向上层 Task 传递结果。promise type 决定异常是在 co_await 处重新抛出,还是表示为错误值。
Coroutine 看起来像顺序代码,实际跨越多个时间点。必须文档化 frame 所有权、executor、恢复线程、取消和错误传播,并测试项目实际采用的 Task 实现。