在 C++20 之后,协程(coroutine)成为语言中一项重要的新特性。它们为异步编程、生成器以及状态机等模式提供了更为直观、类型安全和高效的实现方式。本文从协程的核心概念、关键类型、使用方式、实际示例以及常见坑点进行系统阐述,帮助你快速掌握并在项目中应用协程。
1. 协程到底是什么?
协程是一种轻量级的可挂起函数,可以在执行过程中暂停(co_await、co_yield 或 co_return)并在之后恢复。与传统线程相比,协程是单线程、基于事件循环的异步执行单元,切换开销极低。
1.1 核心语法
| 关键字 | 用途 | 说明 |
|---|---|---|
co_await |
暂停协程,等待某个异步操作完成 | 必须与 awaitable 对象配合使用 |
co_yield |
暂停协程,产生一个值 | 用于生成器(generator) |
co_return |
结束协程并返回值 | 与 return 类似,但可在协程内部多次使用 |
1.2 awaitable、awaiter、promise
- awaitable:协程所等待的对象(如
std::future,std::generator等)。 - awaiter:通过
await_ready,await_suspend,await_resume三个成员函数定义等待逻辑。 - promise:协程的承诺对象,用来存储协程的返回值、异常、状态等。
2. 协程的实现细节
协程本质上是由编译器把一个普通函数拆分成若干状态块,生成一个 state machine。编译器会生成两个关键结构:
- promise_type:定义协程返回类型、错误处理等。
- coroutine_handle:用于操作协程的句柄(挂起、恢复、销毁)。
编译器根据 co_await、co_yield、co_return 的位置自动插入状态切换代码,无需手写状态机。
3. 典型协程用例
3.1 生成器(Generator)
#include <coroutine>
#include <iostream>
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(); }
bool next() { return coro.resume(); }
T value() { return coro.promise().current_value; }
};
generator <int> range(int n) {
for (int i = 0; i < n; ++i)
co_yield i;
}
int main() {
auto gen = range(5);
while (gen.next())
std::cout << gen.value() << ' ';
}
输出:
0 1 2 3 4
3.2 异步 I/O 示例
假设我们有一个异步文件读取 async_read_file,返回 awaitable<std::string>:
struct async_file_reader {
std::string data;
struct awaiter {
async_file_reader* reader;
bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<> h) const {
// 假设异步 I/O 在后台线程完成后调用 resume()
std::thread([=]{
std::this_thread::sleep_for(std::chrono::seconds(1));
reader->data = "Hello, coroutine!";
h.resume();
}).detach();
}
std::string await_resume() const noexcept { return reader->data; }
};
awaiter operator co_await() { return {this}; }
};
async_file_reader async_read_file(const std::string& path) {
// 在真正项目中,这里会发起异步文件读取请求
async_file_reader reader;
co_return reader;
}
async_task <void> demo() {
auto reader = co_await async_read_file("sample.txt");
std::cout << "File content: " << reader.data << '\n';
}
提示:
async_task是用户自定义的 awaitable,用来包装协程入口。常见实现方式是std::future或第三方库(如cppcoro::task)。
4. 常见坑点与最佳实践
| 序号 | 坑点 | 解决方案 |
|---|---|---|
| 1 | 未使用 co_await 语义的 awaitable |
await_ready() 必须返回 false,否则协程会立即完成,导致 co_await 失效。 |
| 2 | 资源泄漏 | 确保 coroutine_handle 在退出前 destroy(),或者使用 generator 的析构自动销毁。 |
| 3 | 异常传播 | 在 promise_type::unhandled_exception() 中手动转发或捕获。 |
| 4 | 跨线程挂起/恢复 | co_await 的 await_suspend() 必须返回一个可复用的句柄。不要在线程中直接 resume() 句柄,除非保证线程安全。 |
| 5 | 性能瓶颈 | 避免在协程内部频繁创建临时对象,使用 std::move 或引用传递。 |
| 6 | 使用标准库 std::generator |
C++23 标准化 std::generator,可直接使用 `std::generator |
| ` 而非自己实现。 |
5. 协程在项目中的落地
- 异步 I/O:将
asio、libuv等库的异步接口包装为awaitable,让业务代码像同步一样书写。 - 生成器:用于迭代大数据集、延迟序列或虚拟序列(如链表、树遍历)。
- 协程池:在高并发服务器中使用协程池管理协程生命周期,减少线程切换开销。
- 游戏循环:协程适合处理游戏事件、动画等时序任务,保持代码可读性。
6. 进一步学习资源
- 《C++20 协程实战》
- cppreference.com 对
std::generator、std::future的详细说明 - “C++ Concurrency in Action” 之 “Coroutines” 章节
- GitHub 上的
cppcoro、asio等协程实现库
结语
协程为 C++ 提供了一种既高效又表达力强的异步编程模型。掌握其基本语法、实现机制以及最佳实践后,你可以轻松编写可读、可维护且性能优异的异步代码。下一个步骤就是将协程融入你现有的项目,体会它带来的便利与性能提升吧。