在 C++20 标准中,协程(coroutine)被正式纳入语言核心,为异步编程提供了强大且语义清晰的工具。相比传统的基于回调或线程池的异步实现,协程可以让代码保持同步风格,同时保留了非阻塞的执行特性。下面我们从协程的基本概念、关键语法、实现细节以及实际应用场景几个方面进行详细阐述。
1. 协程的核心概念
- 挂起与恢复:协程在执行过程中可以被挂起(suspend),随后在某个条件满足时恢复(resume)。挂起点由
co_await、co_yield或co_return决定。 - 状态机:协程的内部状态会被自动保存,编译器将协程转换为一个隐式的状态机。挂起点对应状态机的分支,恢复时从对应状态继续执行。
- 生成器与异步任务:通过
co_yield可以实现生成器(generator),通过co_return可以实现异步任务(async task)。
2. 关键语法与类型
co_await:等待一个 awaitable 对象完成。awaitable 对象必须满足await_ready、await_suspend、await_resume三个成员函数。co_yield:向调用方返回一个值,并挂起协程。适用于生成器模式。co_return:结束协程,返回最终结果。std::suspend_always/std::suspend_never:可用于控制协程在某个点是否挂起。std::future/std::promise:传统的同步/异步组合方式。协程可与这些类型配合使用,实现更简洁的异步等待。
3. 实现细节
3.1 Awaitable 的定义
struct SimpleAwaiter {
bool await_ready() const noexcept { return false; } // 永远挂起
void await_suspend(std::coroutine_handle<> h) const noexcept {
// 在这里启动异步操作,完成后恢复协程
std::thread([h]{
std::this_thread::sleep_for(std::chrono::seconds(1));
h.resume(); // 恢复协程
}).detach();
}
int await_resume() const noexcept { return 42; } // 返回结果
};
3.2 生成器示例
struct Generator {
struct promise_type {
int current_value;
std::suspend_always yield_value(int v) {
current_value = v;
return {};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
Generator get_return_object() {
return Generator{
std::coroutine_handle <promise_type>::from_promise(*this)
};
}
void unhandled_exception() { std::terminate(); }
void return_void() {}
};
std::coroutine_handle <promise_type> coro;
Generator(std::coroutine_handle <promise_type> h) : coro(h) {}
~Generator() { if (coro) coro.destroy(); }
int next() {
if (coro.done()) throw std::runtime_error("end");
coro.resume();
return coro.promise().current_value;
}
};
Generator fib(int n) {
int a = 0, b = 1;
for (int i = 0; i < n; ++i) {
co_yield a;
int temp = a + b;
a = b; b = temp;
}
}
3.3 与 std::future 结合
struct AsyncTask {
struct promise_type {
std::promise <int> prom;
AsyncTask get_return_object() {
return AsyncTask{prom.get_future()};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() { prom.set_exception(std::current_exception()); }
void return_value(int v) { prom.set_value(v); }
};
std::future <int> fut;
AsyncTask(std::future <int> f) : fut(std::move(f)) {}
};
AsyncTask compute(int x) {
int result = x * 2;
co_return result;
}
4. 实际应用场景
- 网络 I/O:使用协程实现异步 socket,避免回调地狱。典型方案是将
asio::awaitable与co_await配合使用。 - 协程任务调度:通过自定义调度器,将协程挂起点注册到事件循环或线程池中。
- 流式数据处理:利用生成器模式,实现惰性序列计算,例如按需读取大文件、生成无限序列等。
- 多任务并行:将多条协程并发执行,并通过
co_await等待全部完成,实现更高层次的并行。
5. 常见陷阱与最佳实践
- 避免过度挂起:每次挂起/恢复都涉及上下文切换,过多的
co_await可能导致性能下降。只在真正需要等待 I/O 或长时间运算时使用。 - 异常传播:协程中的异常会自动通过
promise_type::unhandled_exception传递。需要在外部捕获,或在await_resume中处理。 - 资源管理:协程的生命周期与
coroutine_handle关联。若协程未被显式销毁,可能导致资源泄漏。推荐使用 RAII 包装器。 - 调试困难:协程拆分为多段状态机,单步调试时不易直观。建议结合日志或使用支持协程调试的 IDE。
6. 结语
C++20 的协程为异步编程提供了更直观、类型安全且可维护的方案。它与传统的异步机制相比,能够让程序员保持同步的代码风格,减少回调的层层嵌套。随着编译器和标准库的持续完善,协程将在未来的 C++ 生态中发挥越来越重要的作用。欢迎大家动手实验,尝试用协程重写旧有的异步代码,感受它带来的便利与挑战。