在C++20里,协程(coroutines)被正式纳入标准,提供了更简洁、更高效的异步编程模型。本文将从协程的核心语义、实现原理、常见使用场景以及实际编码示例,系统地拆解协程,让你快速掌握这项强大工具。
1. 协程的核心概念
1.1 什么是协程?
协程是一种比线程更轻量级的用户级并发抽象。它可以在一个函数内部“挂起”和“恢复”,使得函数能够在多次调用之间保持状态。协程与传统的回调或异步链式操作相比,语义更接近同步代码,阅读和维护成本更低。
1.2 协程与生成器的区别
- 生成器(Generator):一次产生一个值,使用
co_yield。 - 任务(Task):返回最终结果,使用
co_return。 - 暂停点:使用
co_await等待异步操作完成。
协程的语法并不局限于生成器或任务,标准库中的std::generator、std::task(非官方)以及自定义协程都可以按需组合。
2. C++20 协程的实现细节
2.1 关键字
co_await:挂起协程,等待 awaitable 对象完成。co_yield:产生一个值并挂起,等待下次co_resume。co_return:结束协程并返回结果。
2.2 协程句柄(std::coroutine_handle)
每个协程都有一个句柄,负责:
- 挂起:
handle.promise().await_suspend(handle) - 恢复:
handle.resume() - 检查完成:
handle.done()
2.3 Promise 对象
Promise 是协程的状态容器,存储协程返回值、异常、挂起点等。promise_type 必须定义:
get_return_object()initial_suspend()final_suspend()return_value(T)unhandled_exception()
3. 常见协程模型
| 模型 | 主要场景 | 典型实现 | 代码片段 |
|---|---|---|---|
| 生成器 | 逐步产生数据流 | `std::generator | |
|co_yield value;` |
|||
| 任务 | 异步 I/O、网络 | `std::future | |
+co_return|co_return result;` |
|||
| 异步 I/O | 文件、网络、数据库 | awaitable + co_await |
auto data = co_await async_read(...); |
| 协作式多任务 | 游戏循环、实时渲染 | scheduler + coroutine_handle |
co_resume; |
4. 实战案例:使用协程实现一个简单的 HTTP 客户端
以下代码演示如何用协程完成一个 GET 请求,并解析响应主体。使用了 cppcoro 库的 awaitable 与 asio。
#include <iostream>
#include <asio.hpp>
#include <cppcoro/awaitable.hpp>
#include <cppcoro/sync_wait.hpp>
using asio::ip::tcp;
using namespace cppcoro;
// 异步 TCP 连接
awaitable<tcp::socket> async_connect(asio::io_context& ctx,
const std::string& host, const std::string& port)
{
tcp::resolver resolver(ctx);
auto results = co_await resolver.async_resolve(host, port, use_awaitable);
tcp::socket socket(ctx);
co_await asio::async_connect(socket, results, use_awaitable);
co_return std::move(socket);
}
// 异步读取 HTTP 响应
awaitable<std::string> async_http_get(tcp::socket& socket,
const std::string& host,
const std::string& path)
{
std::ostream request_stream(&socket);
request_stream << "GET " << path << " HTTP/1.1\r\n";
request_stream << "Host: " << host << "\r\n";
request_stream << "Connection: close\r\n\r\n";
std::string response;
char buffer[1024];
std::size_t n;
while ((n = co_await socket.async_read_some(asio::buffer(buffer), use_awaitable)) > 0)
{
response.append(buffer, n);
}
co_return response;
}
awaitable <void> http_get(const std::string& host,
const std::string& port,
const std::string& path)
{
asio::io_context ctx;
auto socket = co_await async_connect(ctx, host, port);
std::string body = co_await async_http_get(socket, host, path);
std::cout << "Response:\n" << body << '\n';
}
int main()
{
sync_wait(http_get("example.com", "80", "/"));
return 0;
}
关键点说明
use_awaitable让asio返回一个awaitable对象,适配协程。co_await使得异步操作在 I/O 完成后自动恢复,代码保持同步风格。sync_wait用于在main中启动协程并阻塞直到完成。
5. 协程与传统异步方法对比
| 特性 | 传统异步(回调/Future) | C++20 协程 |
|---|---|---|
| 可读性 | 需要嵌套回调或链式 then |
接近同步代码,易读 |
| 错误处理 | 需要捕获异常后手动 propagate | Promise 自动捕获异常 |
| 资源管理 | 手动管理生命周期 | Promise 负责释放资源 |
| 性能 | 线程/线程池开销 | 协程是轻量级上下文切换 |
6. 常见陷阱与调试技巧
- 忘记
co_return- 协程没有返回值时,确保至少
co_return;。
- 协程没有返回值时,确保至少
- Promise 的生命周期
- 如果
awaitable返回引用,保证引用指向的数据在协程结束前有效。
- 如果
- 悬挂协程
- 使用
co_await时要确保 awaitable 能够完成,否则协程永远挂起。
- 使用
- 调试工具
- 使用
gdb或lldb打印std::coroutine_handle,或者利用cppcoro::trace()打印协程执行路径。
- 使用
7. 未来展望
C++23 对协程的支持继续扩展:
std::ranges::views::transform可与协程结合实现惰性流水线。std::task的标准化,使协程与线程池、事件循环无缝集成。- 更丰富的 awaitable 适配器:如
std::future,std::promise的原生协程支持。
随着库生态的完善,协程将在高性能服务器、游戏引擎、嵌入式系统等领域得到更广泛应用。
结语
C++20 协程让异步编程更直观、可维护。理解协程的底层原理,掌握常见模式,并通过实战案例练习,就能在自己的项目中高效使用协程。未来随着标准的演进和生态的成熟,协程将成为 C++ 开发者不可或缺的工具之一。祝你在协程的世界里玩得开心,写出优雅而高效的代码。