协程是 C++20 对异步编程的一大补充,它们让我们能够以同步的方式编写异步逻辑,代码更直观、易维护。本文从协程的实现机制、关键概念到实际示例,系统梳理协程的核心原理与使用技巧。
1. 协程是什么?
协程(coroutine)是一种轻量级的用户态线程,允许在函数内部暂停(co_await、co_yield、co_return)并在随后恢复执行。与传统的回调或 std::async、std::future 等相比,协程能够:
- 保持状态:在挂起点之间保存局部变量。
- 透明控制流:看似同步的写法,内部实现异步。
- 高效切换:协程切换的成本远低于线程切换。
2. 关键概念
| 关键字 | 作用 | 典型用法 |
|---|---|---|
co_await |
暂停协程,等待一个可等待对象(awaitable)。 | int x = co_await some_async_task(); |
co_yield |
暂停协程并返回一个值,后续再次进入时继续执行。 | co_yield i; |
co_return |
结束协程,返回一个值。 | co_return result; |
Awaitable
一个对象若能被 co_await,就称为 awaitable。它至少要实现:
bool await_ready() noexcept;立即完成?void await_suspend(std::coroutine_handle<>) noexcept;挂起时的操作。T await_resume() noexcept;恢复后返回的值。
C++20 标准库提供了一些基础 awaitable,如 std::future, std::generator 等。
Coroutine Handle
协程生成后返回 std::coroutine_handle<>,可用于手动恢复、检查状态、销毁协程。
3. 实现细节
协程在编译阶段被转换为 状态机:
- 生成:编译器为每个
co_*点生成一个分支。 - 挂起:
co_await会把当前栈帧状态保存在 heap 或专用内存结构中。 - 恢复:再次调用
resume()时,编译器会跳转到下一个分支,继续执行。
这就是协程为何能在暂停时保留局部变量的原因——它们不再依赖栈,而是通过堆或专门的 promise 对象维护。
4. 一个完整示例:异步读取文件行
#include <iostream>
#include <fstream>
#include <coroutine>
#include <string>
#include <vector>
// 简易 awaitable,异步读取一行
struct AsyncReadLine {
std::ifstream& stream;
std::string line;
bool await_ready() noexcept { return false; } // 永远挂起
void await_suspend(std::coroutine_handle<> h) noexcept {
std::thread([this, h]() {
if (std::getline(stream, line)) {
// 读取成功,恢复协程
h.resume();
} else {
// 读取失败(EOF),直接恢复
h.resume();
}
}).detach();
}
std::string await_resume() noexcept { return std::move(line); }
};
struct LineReader {
struct promise_type {
std::string current;
std::coroutine_handle <promise_type> next;
std::vector<std::string> buffer;
LineReader get_return_object() {
return LineReader{ std::coroutine_handle <promise_type>::from_promise(*this) };
}
std::suspend_always initial_suspend() noexcept { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
// 生成器:每次 co_yield 当前行
std::suspend_always yield_value(std::string&& value) noexcept {
buffer.push_back(std::move(value));
return {};
}
};
std::coroutine_handle <promise_type> handle;
// 析构释放协程
~LineReader() { if (handle) handle.destroy(); }
// 迭代器实现
struct iterator {
std::vector<std::string> buffer;
size_t idx = 0;
iterator() = default;
bool operator!=(const iterator& other) const { return idx != other.idx; }
const std::string& operator*() const { return buffer[idx]; }
iterator& operator++() { ++idx; return *this; }
};
iterator begin() {
handle.resume(); // 开始执行协程
return iterator{ handle.promise().buffer, 0 };
}
iterator end() { return iterator{}; }
};
LineReader read_lines(std::ifstream& file) {
while (true) {
AsyncReadLine read{file};
std::string line = co_await read;
if (line.empty() && file.eof()) break;
co_yield std::move(line);
}
}
int main() {
std::ifstream infile("example.txt");
if (!infile) return 1;
for (const auto& line : read_lines(infile)) {
std::cout << line << '\n';
}
}
说明
AsyncReadLine是一个 awaitable,内部使用std::thread异步读取文件行。read_lines协程通过co_await挂起等待读取结果,然后co_yield返回每行。LineReader封装协程,提供begin()/end()迭代器,让for-each能直接使用。
5. 与 std::async、std::future 对比
| 维度 | std::async |
C++20 协程 |
|---|---|---|
| 语义 | 线程池/线程封装 | 状态机,轻量级 |
| 锁/竞争 | 需要同步 | 通常不需要 |
| 错误处理 | future::get 抛异常 |
通过 await_resume 抛异常 |
| 代码风格 | 回调式或 Future/Promise | 直观同步式 |
| 性能 | 线程上下文切换 | 只需堆分配 + 函数指针跳转 |
6. 常见坑与最佳实践
- 避免在
await_suspend中使用std::async
std::async本身会创建线程,和协程配合会导致线程堆叠。推荐使用 IO 多路复用(asio、boost::asio)或自定义事件循环。 - 销毁协程
协程句柄不自动销毁,必须显式handle.destroy()或让promise_type的final_suspend负责。 co_return与co_yield混用
co_return用于返回单一结果;co_yield用于生成序列。不要在同一协程里混用,否则会导致逻辑混乱。- 异常安全
await_resume可以抛异常;协程异常在promise_type::unhandled_exception()处处理。若不处理,程序会直接std::terminate()。
7. 结语
C++20 的协程为异步编程带来了前所未有的简洁性和性能。它们既不是回调也不是线程,而是一种状态机,能够在保持同步风格的同时,隐藏掉异步实现细节。掌握协程的基本原理、awaitable 设计以及正确的资源管理,你就能在高并发网络服务、游戏引擎、实时数据处理等领域写出更可维护、更高效的代码。
欢迎大家继续深入探索协程与 asio、std::generator、std::task 的组合使用,开启 C++20 异步编程的新篇章。