60 / 80 · C++20 · 约 15 分钟
Coroutines:暂停、恢复与协程帧所有权
C++20 协程让函数暂停后恢复,但不会自动创建线程、事件循环或后台任务。最小可用抽象也必须明确管理协程帧、完成状态与异常;下面的同步生成器用独占 RAII 所有权保证提前结束时仍能释放资源。
语言机制不等于调度器
函数含有 co_await、co_yield 或 co_return 等协程语法时,编译器按返回类型的 promise_type 协议组织执行。跨暂停点仍需保存的状态进入协程帧,帧通常需要动态存储,但特定条件下分配可以被优化消除。它不是给每个函数配一条完整线程栈。
协程只提供暂停与恢复机制,何时恢复、在哪条线程恢复由外部抽象决定。例子由 main 主动调用 next,因此所有代码同步地在调用线程执行;co_yield 交出一个整数后暂停,并没有启动后台计算,更没有自动并行运行。
句柄不拥有帧,包装器必须拥有
coroutine_handle 是可复制的非拥有句柄,复制它不会复制协程帧。示例把句柄藏在 Generator 内,禁止复制、允许移动,并由析构函数 destroy 帧。initial_suspend 使用 suspend_always,使函数体在首次 next 才运行;final_suspend 同样暂停,让所有者统一负责销毁。
next 先检查空句柄与完成状态,避免恢复已经结束的协程。每次 yield 将整数保存到 promise,next 再按值返回 optional,调用者不持有帧内部元素的引用。移动赋值先释放自身旧帧,再接管新帧,提前离开作用域也走同一条 RAII 释放路径。
异常与借用必须穿过暂停点审查
函数体未处理异常进入 unhandled_exception,示例保存 exception_ptr 并在恢复方重新抛出,避免把失败伪装成普通序列结束。帧仍由 Generator 管理,异常传播不会要求调用者手工 destroy。next 返回空 optional 后再次调用也安全地保持结束状态。
引用参数不会因为进入协程帧就变成拥有副本,this 指针和捕获 lambda 的对象同样可能提前销毁。本例生成函数无外部借用,局部 Guard 用断言展示暂停时存活、提前销毁时释放。这是同步、单消费者的教学生成器,不承诺并发恢复安全。
容易答错的地方
- 不能对已到 final_suspend 的协程再次 resume,也不能由两个复制句柄各自 destroy;独占所有权与完成检查必须同时成立。
- 协程接受引用或依赖 this 时,调用返回不代表相关对象可销毁;它们必须覆盖未来所有恢复操作的生命期。
运行一个例子
最低标准 C++20 · 完整程序 · 下载 .cpp
#include <cassert>
#include <coroutine>
#include <exception>
#include <iostream>
#include <optional>
#include <stdexcept>
#include <utility>
class Generator {
public:
struct promise_type {
int current = 0;
std::exception_ptr error;
Generator get_return_object();
std::suspend_always initial_suspend() noexcept { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
std::suspend_always yield_value(int n) noexcept {
current = n;
return {};
}
void return_void() noexcept {}
void unhandled_exception() noexcept { error = std::current_exception(); }
};
private:
using Handle = std::coroutine_handle<promise_type>;
Handle handle_;
explicit Generator(Handle h) noexcept : handle_(h) {}
public:
Generator(const Generator&) = delete;
Generator& operator=(const Generator&) = delete;
Generator(Generator&& other) noexcept
: handle_(std::exchange(other.handle_, {})) {}
Generator& operator=(Generator&& other) noexcept {
if (this != &other) {
if (handle_) handle_.destroy();
handle_ = std::exchange(other.handle_, {});
}
return *this;
}
~Generator() { if (handle_) handle_.destroy(); }
std::optional<int> next() {
if (!handle_ || handle_.done()) return std::nullopt;
handle_.resume();
if (handle_.promise().error)
std::rethrow_exception(handle_.promise().error);
if (handle_.done()) return std::nullopt;
return handle_.promise().current;
}
};
Generator Generator::promise_type::get_return_object() {
return Generator{std::coroutine_handle<promise_type>::from_promise(*this)};
}
struct Guard {
inline static int live = 0;
Guard() { ++live; }
~Guard() { --live; }
};
Generator numbers() {
Guard guard;
for (int n = 1; n <= 3; ++n) co_yield n;
}
Generator failure() {
throw std::runtime_error("failed");
co_return;
}
int main() {
{
auto early = numbers();
assert(Guard::live == 0);
auto first = early.next();
assert(first && *first == 1 && Guard::live == 1);
auto moved = std::move(early);
assert(!early.next());
}
assert(Guard::live == 0);
auto all = numbers();
int total = 0;
while (auto n = all.next()) total += *n;
assert(total == 6 && Guard::live == 0);
assert(!all.next());
bool caught = false;
try { auto bad = failure(); bad.next(); }
catch (const std::runtime_error&) { caught = true; }
assert(caught);
std::cout << total << ' ' << Guard::live << '\n';
}
在本地编译
g++ -std=c++20 -Wall -Wextra -Wpedantic -pthread modern-coroutines.cpp -o example && ./example预期结果
6 0
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
若把 final_suspend 改为 suspend_never,却仍由 Generator 析构时 destroy,会发生什么所有权问题?
查看参考答案
正常结束后帧可能已自动销毁,Generator 中却还保存原句柄,随后检查或 destroy 就可能访问失效帧。这个包装器的契约依赖最终暂停保留帧,不能只改一个返回类型。要采用自动销毁策略,必须重新设计句柄失效通知与所有权协议,而不是继续复用现有析构逻辑。
继续查证
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。