C++20 引入了协程(coroutines)这一强大而灵活的特性,使得异步编程和生成器的实现变得更加简洁直观。协程本质上是一种能够挂起和恢复执行的函数,它通过 co_await、co_yield 和 co_return 关键字实现状态的保存与恢复。下面,我们从协程的核心概念出发,结合实际案例,阐述如何在 C++20 中使用协程,并说明其优势与适用场景。
1. 协程的基本结构
一个协程函数与普通函数唯一不同的是它的返回类型必须是 std::coroutine_handle、std::future、std::generator 等协程相关类型,或者是一个自定义类型。内部可以使用以下三种关键字:
| 关键字 | 用途 | 说明 |
|---|---|---|
co_await |
等待异步操作完成 | 让协程挂起,直到 awaitable 对象完成 |
co_yield |
生成值 | 把值返回给调用者,同时挂起协程 |
co_return |
返回最终结果 | 结束协程并返回结果 |
协程在执行到 co_await 或 co_yield 时会产生一个挂起点,调用方可以通过 std::coroutine_handle::resume() 继续执行。协程的状态会保存在协程框架中,所有局部变量都会被“暂停”而不会被销毁。
2. 协程与异步 I/O
传统的异步 I/O 需要使用回调、事件循环或 Future/Promise 组合实现。协程通过 co_await 让异步等待变得像同步代码一样直观。下面给出一个简化的网络读取示例:
#include <iostream>
#include <coroutine>
#include <future>
#include <chrono>
struct AwaitableRead {
int socket;
char* buffer;
std::size_t size;
std::chrono::steady_clock::time_point deadline;
bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<> h) {
// 假设我们把读取操作注册到事件循环
// 当数据可读时,事件循环调用 h.resume()
std::cout << "挂起,等待数据...\n";
}
std::size_t await_resume() const noexcept {
// 返回读取字节数
std::cout << "数据已到达\n";
return size;
}
};
std::future<std::size_t> async_read(int socket, char* buf, std::size_t n) {
co_return AwaitableRead{socket, buf, n, std::chrono::steady_clock::now() + std::chrono::seconds(5)};
}
int main() {
char buffer[1024];
auto fut = async_read(1, buffer, 1024);
std::cout << "开始读取\n";
auto bytes = fut.get(); // 这里会阻塞,直到协程完成
std::cout << "读取完成,字节数: " << bytes << "\n";
}
上面代码演示了协程如何与事件循环协作,在异步 I/O 完成时自动恢复执行,避免了显式的回调层叠。
3. 协程生成器(generator)
co_yield 使得实现生成器变得轻而易举。我们可以轻松实现一个斐波那契数列生成器:
#include <iostream>
#include <coroutine>
#include <vector>
template <typename T>
struct generator {
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{std::coroutine_handle <promise_type>::from_promise(*this)};
}
void return_void() {}
void unhandled_exception() { std::exit(1); }
};
std::coroutine_handle <promise_type> coro;
generator(std::coroutine_handle <promise_type> h) : coro(h) {}
~generator() { if (coro) coro.destroy(); }
struct iterator {
std::coroutine_handle <promise_type> coro;
bool operator!=(std::default_sentinel_t) const { return !coro.done(); }
void operator++() { coro.resume(); }
const T& operator*() const { return coro.promise().current_value; }
};
iterator begin() { coro.resume(); return {coro}; }
std::default_sentinel_t end() { return {}; }
};
generator<unsigned long long> fib(unsigned int n) {
unsigned long long a = 0, b = 1;
for (unsigned int i = 0; i < n; ++i) {
co_yield a;
std::tie(a, b) = std::make_pair(b, a + b);
}
}
int main() {
for (auto v : fib(10)) {
std::cout << v << ' ';
}
}
该实现利用 co_yield 暂停协程并返回当前值,调用方通过迭代器逐个获取生成器的值。
4. 协程的优势
| 优势 | 说明 |
|---|---|
| 代码可读性高 | 异步代码写法像同步,逻辑清晰 |
| 资源管理简化 | 协程框架负责状态保存,减少手动管理 |
| 性能提升 | 通过协程调度而非线程切换,减少上下文切换成本 |
| 组合灵活 | 可与 std::future、std::thread 等结合,支持多种异步模型 |
5. 适用场景
- 网络 I/O:在高并发服务器中,协程可以让每个请求保持一个轻量级状态,避免线程数膨胀。
- 生成器:如文件行读取、数据流处理,使用
co_yield轻松实现惰性迭代。 - 游戏循环:协程可以用于实现非阻塞的脚本系统或 AI 行为树。
- 任务调度:在需要细粒度任务切换的实时系统中,协程提供了高效的切换机制。
6. 小结
C++20 的协程为现代 C++ 开发提供了更自然、更高效的异步编程模型。通过 co_await、co_yield 和 co_return,开发者可以在保持代码可读性的同时,构建高性能的异步系统。随着标准库与第三方框架(如 Boost.Asio、cppcoro 等)的不断成熟,协程正逐步成为 C++ 生态中不可或缺的重要工具。