在 C++20 中,协程(Coroutines)被正式纳入标准库,为异步编程、生成器以及复杂的状态机实现提供了强大的语法支持。本文将从协程的基本概念、实现机制,到如何在实际项目中使用协程处理异步 I/O,提供一个完整的学习路径。
1. 协程的基本概念
协程是一种比传统线程更轻量级的并发模型。它可以在执行过程中被挂起(co_await/co_yield/co_return),随后在需要时恢复执行。与线程相比,协程不需要切换堆栈,减少了系统资源消耗和上下文切换开销。
关键术语
| 术语 | 定义 |
|---|---|
| 挂起点 | co_await, co_yield, co_return 的位置 |
协程句柄 (std::coroutine_handle) |
用于控制协程生命周期的对象 |
| 协程 promise | 与协程状态关联的结构体,提供 get_return_object() 等成员 |
| 悬挂/恢复 | 协程被挂起后保持挂起状态,等待恢复 |
2. 协程的实现原理
协程本质上是编译器生成的状态机。co_await 之后的代码会被拆分为多个“分支”,每个分支对应一个生成器的状态。编译器将协程函数展开成一个 promise_type 结构体,内部保存所有需要保存的局部变量以及控制流状态。
代码示例
#include <coroutine>
#include <iostream>
struct Task {
struct promise_type {
Task get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
};
Task myCoroutine() {
std::cout << "Step 1\n";
co_await std::suspend_always{}; // 挂起点
std::cout << "Step 2\n";
co_return;
}
编译后,myCoroutine 变成了一个状态机,co_await 处会生成一个暂停点,后续代码会被包装进 resume 方法中。
3. 典型使用场景
3.1 异步 I/O
协程最常见的用途是包装异步 I/O,例如文件、网络请求。使用 co_await 可以让代码保持同步的写法,同时隐藏异步等待的复杂性。
#include <boost/asio.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/use_awaitable.hpp>
boost::asio::awaitable <void> asyncRead(
boost::asio::ip::tcp::socket& socket,
std::vector <char>& buffer) {
std::size_t n = co_await socket.async_read_some(
boost::asio::buffer(buffer),
boost::asio::use_awaitable);
std::cout << "Read " << n << " bytes\n";
}
3.2 生成器
协程可以用来实现惰性序列,例如斐波那契数列、素数生成器。
#include <coroutine>
#include <iostream>
#include <vector>
template<typename T>
struct Generator {
struct promise_type {
T value_;
std::suspend_always yield_value(T v) {
value_ = v; 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::terminate(); }
};
std::coroutine_handle <promise_type> handle_;
Generator(std::coroutine_handle <promise_type> h) : handle_(h) {}
~Generator() { if (handle_) handle_.destroy(); }
bool next() {
if (!handle_.done()) handle_.resume();
return !handle_.done();
}
T value() const { return handle_.promise().value_; }
};
Generator <int> fibonacci() {
int a = 0, b = 1;
while (true) {
co_yield a;
int c = a + b;
a = b; b = c;
}
}
int main() {
auto gen = fibonacci();
for (int i = 0; i < 10 && gen.next(); ++i)
std::cout << gen.value() << " ";
}
3.3 状态机
协程可以简化复杂状态机的实现,例如游戏 AI 或 UI 事件流。
4. 性能与资源管理
协程的优势在于轻量级,但仍需注意以下几点:
- 内存占用:协程体内所有局部变量会被拆分为成员,导致内存占用不如普通函数。可通过
co_yield只存储必要变量。 - 异常传播:在
promise_type中实现unhandled_exception()可以捕获异常并决定处理方式。若未处理会std::terminate()。 - 调度器:协程本身不包含调度逻辑,需要配合事件循环(如 Boost.Asio、libuv)或自定义调度器。
5. 实际项目案例
5.1 网络服务端
使用 Boost.Asio + C++20 协程实现一个简单的 HTTP 服务器。
#include <boost/asio.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/use_awaitable.hpp>
using namespace boost::asio;
awaitable <void> handleConnection(tcp::socket socket) {
char buffer[1024];
std::size_t n = co_await socket.async_read_some(buffer(buffer), use_awaitable);
std::string request(buffer, n);
std::cout << "Received request: " << request << "\n";
std::string response = "HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\nHello, world";
co_await async_write(socket, buffer(response), use_awaitable);
}
awaitable <void> server(uint16_t port) {
io_context io;
tcp::acceptor acceptor(io, tcp::endpoint(tcp::v4(), port));
for (;;) {
tcp::socket socket = co_await acceptor.async_accept(use_awaitable);
co_spawn(io, handleConnection(std::move(socket)), detached);
}
}
5.2 数据流处理
使用协程来实现一个数据流管道,支持异步读取、处理和写入。
awaitable <void> dataPipeline() {
std::vector <char> buffer(4096);
while (true) {
std::size_t n = co_await async_read_some(source, buffer, use_awaitable);
if (n == 0) break; // EOF
// 处理数据
process(buffer.data(), n);
// 写入
co_await async_write(destination, buffer, use_awaitable);
}
}
6. 常见陷阱与调试技巧
| 问题 | 解决方案 |
|---|---|
| 协程被错误销毁 | 确保 coroutine_handle 只在必要时 destroy();避免悬空引用 |
| 内存泄漏 | 在 promise_type 的 final_suspend() 中释放资源 |
| 调试困难 | 使用 -g 编译并借助 GDB 的 bt 查看协程调用栈;或使用 asio::debug 输出日志 |
| 性能瓶颈 | 对比 std::suspend_always vs std::suspend_never,尽量减少不必要的挂起点 |
7. 未来展望
- 标准库扩展:C++23 正在讨论更完善的协程工具,例如
std::generator、std::async的协程化。 - 跨语言互操作:协程在 Rust、Python、JavaScript 等语言中已经成熟,C++ 通过
cppcoro、asio::awaitable等库与其互操作。 - 硬件加速:未来可能出现针对协程的专用指令集或调度器,以进一步提升性能。
结语
C++20 协程为处理异步 I/O、生成器以及状态机提供了更加简洁、可维护的语法。虽然学习曲线略陡峭,但掌握后能显著提升代码可读性和运行效率。希望本文能帮助你快速上手协程,在实际项目中发挥其优势。祝编码愉快!