C++20 在标准库中正式引入了协程(Coroutines)这一强大的语言特性,旨在简化异步编程、生成器以及延迟计算等模式。与传统的线程、回调或手写状态机相比,协程提供了一种更直观、更高效、更易维护的写法。下面从定义、实现原理、典型使用场景以及常见坑等方面进行系统剖析。
一、协程的基本概念
-
协程(Coroutine)是一种可暂停、可恢复的函数。它允许在函数内部“挂起”执行,稍后再恢复,而不必返回到调用者的栈帧。与线程不同,协程在同一线程中切换,避免了线程上下文切换的成本。
-
C++ 的协程使用关键字
co_await,co_yield,co_return等来声明挂起点,并且通过 promise 与 awaitable 两个核心概念来连接协程与外部的调度器。 -
协程本质上是一个状态机,编译器会把含有挂起点的函数编译为一个生成器类,该类拥有内部状态、栈帧和继续点信息。
二、协程实现的工作流程
-
定义协程函数
std::future <int> asyncAdd(int a, int b) { std::this_thread::sleep_for(std::chrono::seconds(1)); co_return a + b; // 立即返回,协程结束 } -
协程的调用
调用协程返回一个 awaitable 对象(如std::future或自定义generator)。该对象不立即执行函数,而是保存协程入口点。 -
等待协程
调用者通过co_await或.get()等方式,触发协程的实际执行。执行过程中遇到co_await会挂起,直到 awaitable 完成;遇到co_yield会返回一个值给调用者,并暂停。 -
调度器
对于std::future等标准实现,调度器是基于线程池的。自定义协程往往需要实现自己的调度器,用于把协程挂起点关联到事件循环(如asio的io_context)或自定义任务队列。
三、典型使用场景
| 场景 | 传统实现 | 协程实现 | 优点 |
|---|---|---|---|
| 异步 I/O | 回调 + 状态机 | co_await + asio::awaitable |
代码更像同步,错误处理更自然 |
| 生成器 | 手写迭代器 | co_yield |
简洁、可读性高 |
| 协同多任务 | 线程 + 互斥 | 协程 + 事件循环 | 低延迟、低资源占用 |
| 延迟计算 | 函数返回 std::function | co_return |
更易组合、链式调用 |
四、常见陷阱与最佳实践
-
不当的 awaitable
- 问题:在协程中
co_await一个永不完成的 awaitable 会导致程序挂起。 - 解决:确保所有 awaitable 有明确的完成路径,或使用超时/取消机制。
- 问题:在协程中
-
内存泄漏
- 问题:协程内部的对象可能被延迟析构,导致资源在协程挂起期间未释放。
- 解决:使用 RAII,确保所有资源在协程作用域内及时析构;或者显式在
finally块中释放。
-
异常传播
- 问题:异常在协程内部抛出后,会被封装进
std::future的exception_ptr,调用者需要显式获取。 - 解决:在调用
co_await或.get()前使用try/catch,并注意异常是否已经被捕获。
- 问题:异常在协程内部抛出后,会被封装进
-
性能开销
- 问题:协程生成的状态机类体积较大,频繁创建会产生 GC 与分配开销。
- 解决:复用协程对象(如使用
generator对象池),或将协程定义为单次使用。
-
调试困难
- 问题:协程内部暂停点难以定位。
- 解决:使用支持协程调试的 IDE(如 CLion 2023.1+),或在关键挂起点插入日志。
五、实战示例:基于 asio 的协程 HTTP 客户端
#include <boost/asio.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/asio/awaitable.hpp>
#include <iostream>
using namespace boost::asio;
using tcp = ip::tcp;
awaitable<std::string> fetch(const std::string& host, const std::string& path) {
auto executor = co_await this_coro::executor;
tcp::resolver resolver(executor);
auto const results = co_await resolver.async_resolve(host, "http", use_awaitable);
tcp::socket socket(executor);
co_await async_connect(socket, results, use_awaitable);
// 发送 HTTP GET 请求
std::string req = "GET " + path + " HTTP/1.1\r\nHost: " + host + "\r\nConnection: close\r\n\r\n";
co_await async_write(socket, buffer(req), use_awaitable);
// 接收响应
std::string resp;
boost::system::error_code ec;
while (true) {
std::array<char, 512> buf;
std::size_t n = co_await async_read(socket, buffer(buf), use_awaitable, ec);
if (ec == error::eof) break;
resp.append(buf.data(), n);
}
co_return resp;
}
int main() {
io_context ctx;
co_spawn(ctx, []() -> awaitable <void> {
auto body = co_await fetch("example.com", "/");
std::cout << body.substr(0, 200) << "...\n";
}, detached);
ctx.run();
}
此示例展示了如何用协程完成一个完整的异步 HTTP GET 请求。与传统回调方式相比,代码更简洁、可读。
六、总结
C++20 的协程为异步编程提供了语言级别的原生支持,使得之前需要手写状态机、回调或使用第三方库的场景可以用更直观、更高效的方式实现。虽然协程在引入时会带来一定学习成本,但掌握后能够显著提升代码质量与运行性能。建议从生成器或简单的协程任务入手,逐步引入复杂的异步 I/O 框架,形成自己的协程编程习惯。