在C++20标准中,协程(coroutines)正式成为语言特性,为异步编程和生成器提供了更为自然和高效的语法。与传统的异步框架相比,协程的实现更接近同步代码的写法,同时在性能上也有显著提升。本文将从协程的底层工作机制入手,讲解其实现原理,并结合实例展示如何在实际项目中使用协程实现高效、可读性强的异步代码。
1. 协程的核心概念
协程是可挂起的函数,允许在执行过程中暂停(co_await、co_yield、co_return)并在未来某个时间点恢复。相比传统线程,协程的上下文切换成本极低(仅仅是保存/恢复寄存器和栈指针),而线程的切换需要操作系统调度和栈拷贝,开销远大。
协程由以下几部分组成:
- promise对象:保存协程状态,包括返回值、异常、协程的生命周期控制等。
- awaiter:实现协程挂起的具体逻辑,包含
await_ready()、await_suspend()和await_resume()。 - coroutine handle:
std::coroutine_handle用于管理协程的生命周期和挂起/恢复。
2. 协程的编译后结构
编译器在看到co_await、co_yield等关键字时,会对函数进行“拆分”,将函数体拆成若干状态机片段。具体步骤:
- 生成状态机类:编译器会生成一个内部状态机类,包含一个
promise_type嵌套类以及所有局部变量的存储空间。 - 生成
promise_type:promise_type实现协程的控制逻辑,例如get_return_object()返回协程句柄,initial_suspend()决定协程是否立即挂起,final_suspend()决定协程完成后是否挂起等。 - 生成
awaitable:协程体内的每一次co_await会生成一个awaitable对象,协程会根据await_ready()结果决定是否挂起,若挂起则调用await_suspend()把协程句柄传进去,让外部实现挂起逻辑。
整个过程类似于生成一个自动机:每个co_yield或co_await对应一个状态转移点,编译器通过switch语句或函数指针表来实现。
3. 典型协程使用模式
3.1 生成器(Generator)
#include <coroutine>
#include <iostream>
template<typename T>
struct Generator {
struct promise_type;
using handle_type = std::coroutine_handle <promise_type>;
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{handle_type::from_promise(*this)};
}
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
handle_type coro;
Generator(handle_type h) : coro(h) {}
~Generator() { if (coro) coro.destroy(); }
T next() {
coro.resume();
return coro.promise().current_value;
}
bool done() { return !coro || coro.done(); }
};
Generator <int> count_to(int n) {
for (int i = 0; i < n; ++i)
co_yield i;
}
int main() {
auto gen = count_to(5);
while (!gen.done())
std::cout << gen.next() << " ";
}
此示例演示了协程如何实现一个简易的整数生成器。co_yield会让协程挂起并返回当前值,直到下次调用next()恢复。
3.2 异步IO
使用C++20标准库std::future与协程配合,可以简化异步IO操作:
#include <coroutine>
#include <future>
#include <iostream>
std::future <int> async_add(int a, int b) {
co_return a + b; // 立即返回结果
}
int main() {
auto fut = async_add(3, 4);
std::cout << "Result: " << fut.get() << '\n';
}
更复杂的异步操作,例如网络请求,需要自定义awaitable类型,挂起协程直到事件完成。
4. 使用协程的常见陷阱
| 陷阱 | 说明 | 解决方案 |
|---|---|---|
| 生命周期管理 | promise对象与协程句柄的生命周期需一致,否则访问悬空对象导致崩溃。 | 使用std::coroutine_handle或std::future包装,确保协程完成后自动销毁。 |
| 异常传播 | 协程内部抛出的异常会传递到promise的unhandled_exception(),默认行为是std::terminate()。 |
在promise中实现unhandled_exception(),捕获异常并封装到std::future或自定义错误码。 |
| 性能瓶颈 | 每次co_await都涉及await_suspend和await_resume调用,过度细粒度的挂起会影响性能。 |
将相关操作合并到一个awaitable中,减少上下文切换。 |
5. 与传统异步框架的对比
| 特性 | 传统框架(如Boost.Asio) | C++20协程 |
|---|---|---|
| 上下文切换 | 线程或事件循环 | 栈帧切换(几百字节) |
| 代码可读性 | 回调/状态机 | 直观同步写法 |
| 错误处理 | 复杂链式回调 | try/catch直接捕获 |
协程将异步代码写成同步样式,降低了回调地狱,并且由于编译器优化,往往比手写状态机更高效。
6. 实战示例:异步HTTP客户端
下面演示如何使用协程实现一个简单的异步HTTP GET请求。这里假设已有一个基于Boost.Asio的awaitable类型tcp::async_connect和tcp::async_read_some。
#include <boost/asio.hpp>
#include <boost/asio/awaitable.hpp>
#include <iostream>
namespace asio = boost::asio;
using tcp = asio::ip::tcp;
asio::awaitable<std::string> async_http_get(const std::string& host, const std::string& path) {
auto executor = co_await asio::this_coro::executor;
tcp::resolver resolver(executor);
auto endpoints = co_await resolver.async_resolve(host, "http", asio::use_awaitable);
tcp::socket socket(executor);
co_await asio::async_connect(socket, endpoints, asio::use_awaitable);
std::string request = "GET " + path + " HTTP/1.1\r\n" +
"Host: " + host + "\r\n" +
"Connection: close\r\n\r\n";
co_await asio::async_write(socket, asio::buffer(request), asio::use_awaitable);
asio::streambuf buffer;
std::size_t bytes_transferred;
try {
while ((bytes_transferred = co_await asio::async_read(socket, buffer, asio::transfer_at_least(1), asio::use_awaitable)) != 0) {
std::cout << std::string_view(asio::buffer_cast<const char*>(buffer.data()), bytes_transferred);
buffer.consume(bytes_transferred);
}
} catch (const asio::system_error& e) {
if (e.code() != asio::error::eof) throw;
}
co_return std::string(); // 这里可返回完整响应
}
int main() {
asio::io_context io_ctx;
asio::co_spawn(io_ctx, async_http_get("www.example.com", "/"), asio::detached);
io_ctx.run();
}
该示例展示了协程如何与ASIO的awaitable一起使用,代码与传统回调方式相比简洁且易于维护。
7. 结语
C++20协程通过将异步逻辑融入语言层面,极大提升了代码可读性与可维护性。虽然协程本身是一种复杂的语言特性,但只要掌握其基本原理和常见使用模式,开发者就能在实际项目中轻松实现高效的异步程序。随着标准库和第三方库的完善,协程将成为C++开发者工具箱中不可或缺的一员。