C++20正式加入协程(coroutines)后,协程在语言层面得到了标准化,使得实现异步、生成器等复杂控制流变得异常简单。本文将带你从C++17的协程实验性实现说起,探讨协程的底层原理、常见用法以及与传统异步模型(如线程+future、回调)的区别与优劣。
1. 何为协程?
协程是一种轻量级的用户级线程,允许函数在执行过程中“挂起”(yield)并在需要时恢复。与线程不同,协程的切换是由程序显式控制的,几乎不需要上下文切换开销。协程分为三类:
- 生成器:每次返回一个值后挂起;
- 异步:等待某些事件完成后恢复;
- 协作式:多个协程在运行时通过显式
co_await或co_yield进行切换。
2. C++17 的协程实验性实现
C++17 为协程提供了“预编译器”功能,主要通过 coro.h 头文件实现,编译器支持 co_await、co_yield、co_return。然而该实验版本不完整,缺少关键类型(如 std::experimental::coroutine_handle)的完整实现,且只能通过第三方库(如 Boost.Coroutine2)来使用。
典型的实验性协程代码(需 Boost.Coroutine2):
#include <boost/coroutine2/all.hpp>
#include <iostream>
void generator(boost::coroutines2::coroutine <void>::push_type &sink) {
for (int i = 0; i < 5; ++i) {
sink(i); // co_yield 的实现
}
}
int main() {
boost::coroutines2::coroutine <void>::pull_type source(
[](boost::coroutines2::coroutine <void>::push_type &sink) {
generator(sink);
});
while (source) {
std::cout << source.get() << '\n';
++source; // 手动切换
}
}
虽然能实现生成器,但缺乏语言级语法糖和更复杂的异步功能。
3. C++20 协程的完整实现
C++20 提供了标准化的协程库,核心类为 std::coroutine_handle,并引入了 std::suspend_always、std::suspend_never 等默认挂起策略。协程函数的返回类型必须是支持 co_await 的 awaitable。
3.1 生成器实现
#include <coroutine>
#include <iostream>
template<typename T>
struct generator {
struct promise_type;
using handle_type = std::coroutine_handle <promise_type>;
struct promise_type {
T current_value;
std::suspend_always yield_value(T value) {
current_value = value;
return {};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
generator get_return_object() {
return generator{handle_type::from_promise(*this)};
}
void unhandled_exception() { std::exit(1); }
void return_void() {}
};
handle_type coro;
generator(handle_type h) : coro(h) {}
~generator() { coro.destroy(); }
struct iterator {
handle_type coro;
bool operator==(std::default_sentinel_t) const noexcept { return !coro || coro.done(); }
iterator &operator++() { coro.resume(); return *this; }
T operator*() const { return coro.promise().current_value; }
};
iterator begin() { return iterator{coro}; }
std::default_sentinel_t end() { return {}; }
};
generator <int> range(int start, int end) {
for (int i = start; i <= end; ++i)
co_yield i;
}
int main() {
for (auto v : range(1, 5))
std::cout << v << ' ';
}
这段代码几乎与传统循环一样直观,且编译器自动管理协程栈。
3.2 异步任务(async)
C++20 标准库中 std::future 已被 std::future(已支持异步)与 std::promise 结合使用。协程可以返回 std::future,使用 co_await 等待异步结果。
#include <coroutine>
#include <future>
#include <iostream>
std::future <int> async_add(int a, int b) {
co_return a + b;
}
int main() {
auto fut = async_add(3, 4);
std::cout << "Result: " << fut.get() << '\n';
}
此处 co_return 自动将结果封装为 std::future,无需手动包装。
4. 协程与传统异步模型的对比
| 特点 | 协程 | 线程+future | 回调 |
|---|---|---|---|
| 切换成本 | 轻量级,栈已在堆上 | 上下文切换开销大 | 需要手动维护状态机 |
| 可读性 | 接近同步代码 | 可读性一般 | 嵌套回调导致“回调地狱” |
| 错误处理 | 通过 try/catch 统一 |
同样可使用 try/catch |
需在每层回调中处理 |
| 资源管理 | 通过 RAII 自动 | 需显式锁/同步 | 难以保证释放 |
协程在高并发 I/O 密集型程序(如网络服务器)中表现尤为突出:相比起大量线程,它们占用更少资源并保持代码直观。
5. 常见坑与最佳实践
-
栈大小限制
虽然协程的栈在堆上,但若协程内部递归深度过大,仍会导致堆栈耗尽。应避免深递归或使用循环代替。 -
异常传播
若协程内部抛出异常,co_return不会捕获,需要在promise_type::unhandled_exception中处理。 -
同步与异步的混用
同步函数调用异步协程时,要注意co_await的上下文。最好在统一的事件循环中执行协程。 -
调试工具
目前主流 IDE 对协程调试支持有限,建议使用-fno-omit-frame-pointer编译参数,并配合llvm-symbolizer。
6. 结语
C++20 的协程为现代 C++ 开发者提供了一种既轻量又高效的并发编程模型。通过把异步逻辑写成看似同步的代码,降低了复杂性并提升了可维护性。虽然协程在 I/O 密集型场景中表现突出,但在 CPU 密集型计算时,线程仍是更优选择。掌握协程的使用后,你将能写出更简洁、更高性能的 C++ 程序。