在 C++20 标准中,协程(coroutine)被正式纳入语言规范,提供了一种优雅的方式来实现异步编程、生成器和并发控制。本文将从协程的基本概念、关键字与类型、使用步骤以及常见陷阱四个方面进行阐述,帮助读者快速上手并避免常见错误。
1. 协程的基本概念
协程是一种能够暂停和恢复执行的函数。不同于传统的线程,协程在同一线程中协作完成任务,切换成本极低。协程通过 挂起(suspend) 与 恢复(resume) 的机制,隐藏了底层的状态机实现。
C++20 将协程拆分为三部分:
- 协程函数(coroutine function),使用
co_await,co_yield,co_return关键字。 - 协程生成器(promise type),定义协程的生命周期与返回值。
- 协程句柄(coroutine handle),用于手动恢复、检查状态或销毁协程。
2. 关键字与相关类型
| 关键字 | 用途 | 备注 |
|---|---|---|
co_await |
暂停协程,等待一个 awaitable 对象完成 | 只能在协程内部使用 |
co_yield |
暂停协程并返回一个值给调用者 | 生成器函数常用 |
co_return |
结束协程并返回最终结果 | 只能在协程内部使用 |
co_await 的可等待对象(awaitable) |
必须满足 await_ready, await_suspend, await_resume 三个成员函数 |
也可以是 std::future、std::experimental::future 等 |
核心类型:
- `std::coroutine_handle `:协程句柄,`PromiseT` 是对应的 promise 类型。
std::suspend_always/std::suspend_never:内置的 awaitable,用于控制协程的挂起与恢复。
3. 一个完整的协程示例
下面给出一个简单的整数序列生成器(类似 iota),使用 co_yield 输出每个值。
#include <coroutine>
#include <iostream>
#include <optional>
struct Iota {
struct promise_type {
std::optional <int> value; // 当前产出值
Iota get_return_object() { // 返回协程句柄
return Iota{std::coroutine_handle <promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() { return {}; } // 立即挂起,等待 resume
std::suspend_always final_suspend() noexcept { return {}; } // 结束后挂起
void unhandled_exception() { std::terminate(); }
void return_void() {}
};
std::coroutine_handle <promise_type> handle; // 内部句柄
explicit Iota(std::coroutine_handle <promise_type> h) : handle(h) {}
~Iota() { if (handle) handle.destroy(); }
struct iterator {
std::coroutine_handle <promise_type> h;
bool first = true;
iterator(std::coroutine_handle <promise_type> h) : h(h) {}
iterator& operator++() { // 触发协程恢复
h.resume();
return *this;
}
std::optional <int> operator*() const { return h.promise().value; }
bool operator!=(const iterator& rhs) const { return h != rhs.h; }
};
iterator begin() {
handle.resume(); // 第一次恢复,执行到第一个 co_yield
return iterator{handle};
}
iterator end() { return iterator{handle}; }
};
// 协程函数
Iota make_iota(int start, int count) {
for (int i = 0; i < count; ++i) {
co_yield start + i; // 输出当前值
}
}
int main() {
for (int v : make_iota(5, 10)) { // 输出 5..14
std::cout << v << ' ';
}
return 0;
}
关键点
promise_type::get_return_object()返回一个包装了协程句柄的对象,供外部使用。initial_suspend()与final_suspend()控制协程何时挂起与结束。co_yield将值放入promise_type的成员value,随后挂起。
4. 常见陷阱与解决办法
| 陷阱 | 说明 | 解决办法 |
|---|---|---|
| 协程句柄未销毁 | 协程结束后句柄未显式销毁,导致资源泄漏 | 在 promise_type::final_suspend() 或者外部对象析构时调用 handle.destroy() |
| 无限挂起 | co_await 的 awaitable 没有实现 await_ready() 或 await_resume(),导致永远挂起 |
确保 awaitable 对象实现所有三方法,且 await_ready() 能够返回 true 或者 false 以触发挂起 |
| 错误的协程返回类型 | 直接返回 int 或 void,导致编译错误 |
协程函数返回 `std::coroutine_handle |
| ` 或自定义包装类 | ||
| 多次 resume 后异常 | 对已结束的协程再次 resume,导致 std::bad_function_call |
在每次 resume 前检查 handle.done() |
| 协程不支持递归 | 递归调用同一协程会导致栈空间耗尽 | 通过循环或异步调度实现递归逻辑,避免深层嵌套 |
5. 结合 async/await 与 std::future
C++20 的 co_await 与 std::future 并不直接兼容。若想在协程中等待 std::future,需要自定义一个 awaitable 包装器:
template<typename T>
struct FutureAwaiter {
std::future <T> fut;
bool await_ready() const noexcept { return fut.wait_for(std::chrono::seconds(0)) == std::future_status::ready; }
void await_suspend(std::coroutine_handle<> h) {
std::thread([h, f=std::move(fut)]() mutable {
f.wait();
h.resume();
}).detach();
}
T await_resume() { return fut.get(); }
};
template<typename T>
FutureAwaiter <T> make_awaiter(std::future<T> f) { return FutureAwaiter<T>{std::move(f)}; }
使用时:
int result = co_await make_awaiter(std::async(std::launch::async, []{ return 42; }));
6. 小结
- C++20 协程通过
co_await,co_yield,co_return关键字以及 Promise/Handle 机制实现。 - 关键点在于
promise_type的实现和协程句柄的管理。 - 典型的协程使用场景包括生成器、异步 I/O、任务调度等。
- 关注资源释放、挂起条件和协程生命周期是避免常见错误的关键。
通过上述示例与技巧,读者应能快速构建自己的协程,并在项目中发挥其优势。祝编码愉快!