协程(Coroutines)是 C++20 里最激动人心的特性之一,它让异步编程与同步代码的写法无缝结合,降低了回调地狱的概率。本文从协程的基本概念讲起,逐步展开实现细节、典型应用场景以及常见陷阱,帮助读者快速上手并掌握协程的核心技巧。
一、协程的基本概念
协程是一种能够暂停与恢复执行的函数。与传统的线程相比,协程的切换成本极低,几乎可以忽略不计;与回调函数相比,协程的代码结构更像同步写法,易于维护。C++20 为协程提供了三大语法工具:
co_await:等待一个 awaitable 对象完成。co_yield:在生成器中产生一个值并暂停。co_return:结束协程并返回最终结果。
awaitable 对象
协程只能暂停在可等待的对象上。C++ 标准库提供了 std::future、std::async 等 awaitable,第三方库如 cppcoro::generator 也提供了相应实现。一个自定义 awaitable 需要实现 await_ready()、await_suspend()、await_resume() 三个成员函数。
二、协程的核心实现
下面给出一个最小可复现的协程示例:一个异步读取文件内容的函数。
#include <iostream>
#include <coroutine>
#include <thread>
#include <chrono>
struct AwaitableSleep {
std::chrono::milliseconds duration;
AwaitableSleep(std::chrono::milliseconds d) : duration(d) {}
bool await_ready() noexcept { return false; }
void await_suspend(std::coroutine_handle<> h) {
std::thread([h, this](){
std::this_thread::sleep_for(duration);
h.resume();
}).detach();
}
void await_resume() noexcept {}
};
struct AsyncReadFile {
struct promise_type {
std::string data;
std::exception_ptr eptr;
AsyncReadFile get_return_object() { return AsyncReadFile{ std::coroutine_handle <promise_type>::from_promise(*this) }; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() { eptr = std::current_exception(); }
void return_void() {}
};
std::coroutine_handle <promise_type> handle;
explicit AsyncReadFile(std::coroutine_handle <promise_type> h) : handle(h) {}
~AsyncReadFile() { if (handle) handle.destroy(); }
std::string get() {
if (handle.promise().eptr) std::rethrow_exception(handle.promise().eptr);
return handle.promise().data;
}
};
AsyncReadFile readFileAsync(const std::string& path) {
// 模拟异步读取
co_await AwaitableSleep{ std::chrono::milliseconds(500) };
// 这里应该真正读取文件,但为演示省略
std::string fakeContent = "Hello from " + path;
co_return;
}
关键点拆解
- promise_type:协程的核心,管理状态、返回值与异常。
- initial_suspend / final_suspend:决定协程何时暂停/恢复。
suspend_never让协程立即开始,suspend_always让协程在结束后暂停等待外部销毁。 - await_suspend:在此实现真正的异步操作,例如
std::thread或asio::awaitable。
三、生成器(Generator)实例
生成器是协程最常见的用途之一,能够一次产生一个值而不需要一次性生成整个序列。C++20 标准库本身不直接提供生成器,但可以用协程轻松实现。
template<typename T>
struct generator {
struct promise_type {
T current_value;
std::suspend_always yield_value(T val) {
current_value = std::move(val);
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::rethrow_exception(std::current_exception()); }
};
std::coroutine_handle <promise_type> handle;
explicit generator(std::coroutine_handle <promise_type> h) : handle(h) {}
~generator() { if (handle) handle.destroy(); }
bool next() { return handle.resume(), !handle.done(); }
T current() const { return handle.promise().current_value; }
};
generator <int> naturalNumbers(int start = 1) {
int i = start;
while (true) {
co_yield i++;
}
}
使用示例:
auto gen = naturalNumbers(10);
for (int i = 0; i < 5 && gen.next(); ++i)
std::cout << gen.current() << ' '; // 输出 10 11 12 13 14
四、协程的典型应用场景
- 异步 I/O:与网络、文件等 IO 结合,避免阻塞线程。
- 流式数据处理:如日志、传感器数据,按需生成处理。
- 状态机实现:协程内部可以维护状态,外部通过
next()切换状态。 - 协程管道:多个协程串联,形成数据处理流水线,类似 Go 的 channel。
五、常见陷阱与调优建议
| 陷阱 | 原因 | 解决方案 |
|---|---|---|
| 协程对象被提前销毁 | 协程返回值未被拷贝或移动,导致 handle 被销毁。 | 在使用前确保保存 handle 或返回 std::unique_ptr<generator<T>>。 |
| 内存泄漏 | 协程内部捕获的大对象未在 final_suspend 清理。 |
在 promise_type 里使用 std::shared_ptr 或 unique_ptr 管理资源。 |
| 高开销的线程切换 | await_suspend 里启动大量 std::thread。 |
采用线程池或异步事件循环(如 asio)。 |
| 异常不透明 | unhandled_exception 只存储了指针,调用者需手动 rethrow_exception。 |
在协程返回前提供 catch 或 get() 方法自动抛出。 |
| 调试困难 | 协程内部状态难以在调试器中跟踪。 | 使用 std::suspend_always 代替 suspend_never,让调试器更容易跟踪。 |
六、未来展望
C++23 将继续完善协程支持,预计会推出更友好的生成器标准库、awaitable 标准类型和协程调度器接口。与此同时,第三方生态(如 cppcoro、awaitable)正逐渐成熟,为 C++ 开发者提供更丰富的异步工具链。
通过本文的介绍,读者已经掌握了 C++20 协程的基础语法、实现细节、典型用例与常见陷阱。下一步可以尝试将协程与网络库(如 Boost.Beast 或 cppcoro)结合,实现高性能的异步服务器,进一步体会协程带来的便利。祝编码愉快!