C++20协程
00:00
C++20协程详解:co_await、co_yield、co_return。
概述
C++20 引入了原生协程支持,为异步编程和惰性求值提供了语言级别的基础设施。协程是一种可以暂停执行并在后续恢复的函数,通过 co_await、co_yield 和 co_return 三个关键字实现函数的挂起与恢复。C++20 协程属于无栈协程(Stackless Coroutine),其状态保存在堆上分配的协程帧中,相比有栈协程开销更低。
协程的核心价值在于:让异步代码拥有同步代码的可读性,同时保持异步非阻塞的执行效率。在网络编程、IO 密集型应用和生成器场景中,协程能显著简化代码逻辑。
基础概念
协程关键字
co_await:挂起协程,等待异步操作完成并获取结果co_yield:挂起协程并产出一个值,协程可在后续恢复继续执行co_return:从协程返回,标志协程执行结束
协程的生命周期
一个协程函数被调用时,编译器会自动创建协程帧,其中包含以下核心组件:
- promise_type:控制协程行为的策略对象,定义挂起、恢复和返回的行为
- coroutine_handle:协程句柄,用于外部恢复或销毁协程
- 协程帧:存储局部变量、参数和挂起点的状态
协程与普通函数的区别
普通函数一旦开始执行就会运行到结束,而协程可以在执行过程中暂停,将控制权交还给调用者,并在稍后从暂停的位置继续执行。函数体内只要出现了 co_await、co_yield 或 co_return 中的任何一个,该函数就被编译器识别为协程。
快速上手
最简单的 Generator
#include <coroutine>
#include <iostream>
// 一个简化的 Generator 实现
template<typename T>
struct Generator {
struct promise_type {
T current_value;
Generator get_return_object() {
return Generator{std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
std::suspend_always yield_value(T value) {
current_value = value;
return {};
}
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle<promise_type> handle;
// 迭代器支持 range-based for
struct iterator {
std::coroutine_handle<promise_type> handle;
bool operator!=(std::default_sentinel_t) const { return !handle.done(); }
iterator& operator++() { handle.resume(); return *this; }
T operator*() const { return handle.promise().current_value; }
};
iterator begin() { handle.resume(); return {handle}; }
std::default_sentinel_t end() { return {}; }
};
// 使用 co_yield 生成整数序列
Generator<int> range(int start, int end) {
for (int i = start; i < end; ++i) {
co_yield i;
}
}
int main() {
for (auto val : range(0, 5)) {
std::cout << val << " "; // 输出: 0 1 2 3 4
}
return 0;
}
详细用法
协程 Promise 类型
promise_type 是协程行为的核心控制器,每个协程都必须定义关联的 promise_type:
struct Task {
struct promise_type {
// 创建协程返回对象
Task get_return_object() {
return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
}
// 初始挂起策略:suspend_never 表示协程立即开始执行
std::suspend_never initial_suspend() { return {}; }
// 最终挂起策略:suspend_never 表示协程结束后自动销毁
std::suspend_never final_suspend() noexcept { return {}; }
// co_return 对应的处理
void return_void() {}
// 异常处理
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle<promise_type> handle;
};
co_await 机制详解
co_await 的执行流程分为三个步骤:
auto result = co_await awaitable;
// 步骤 1: 调用 awaitable.await_ready()
// 如果返回 true,表示结果已就绪,直接获取结果,不挂起协程
// 步骤 2: 如果 await_ready() 返回 false,调用 awaitable.await_suspend(handle)
// handle 是当前协程的句柄,可在此安排恢复协程的时机
// 步骤 3: 协程恢复时,调用 awaitable.await_resume() 获取最终结果
自定义 Awaitable 对象:
#include <chrono>
#include <thread>
// 自定义延时等待对象
struct Delay {
int milliseconds;
// 检查是否已就绪(始终返回 false,需要等待)
bool await_ready() const { return false; }
// 挂起协程,在另一个线程上延时后恢复
void await_suspend(std::coroutine_handle<> handle) const {
std::thread([handle, ms = milliseconds]() {
std::this_thread::sleep_for(std::chrono::milliseconds(ms));
handle.resume(); // 延时结束后恢复协程
}).detach();
}
// 恢复后获取结果(此处无返回值)
void await_resume() const {}
};
// 使用自定义 Awaitable
Task delayedGreeting() {
std::cout << "开始等待..." << std::endl;
co_await Delay{1000}; // 等待 1000 毫秒
std::cout << "等待结束!" << std::endl;
}
co_yield 与生成器
co_yield value 等价于 co_await promise.yield_value(value),用于惰性生成值序列:
// 斐波那契数列生成器
Generator<int> fibonacci() {
int a = 0, b = 1;
while (true) {
co_yield a;
auto temp = a;
a = b;
b = temp + b;
}
}
// 使用 take 视图获取前 10 个斐波那契数
int main() {
int count = 0;
for (auto val : fibonacci()) {
std::cout << val << " ";
if (++count >= 10) break; // 输出: 0 1 1 2 3 5 8 13 21 34
}
return 0;
}
常见场景
异步任务链
// 模拟异步网络请求
struct AsyncRead {
std::string data;
bool await_ready() { return !data.empty(); }
void await_suspend(std::coroutine_handle<> h) {
// 模拟异步 IO 完成后恢复
std::thread([this, h]() {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
data = "服务端响应数据";
h.resume();
}).detach();
}
std::string await_resume() { return data; }
};
Task fetchData() {
std::cout << "发起请求..." << std::endl;
auto response = co_await AsyncRead{}; // 异步等待
std::cout << "收到: " << response << std::endl;
}
惰性数据管道
// 按行读取日志文件并过滤
Generator<std::string> readLines(const std::string& filename) {
std::ifstream file(filename);
std::string line;
while (std::getline(file, line)) {
co_yield line;
}
}
// 使用时按需处理,不会一次性加载全部内容
for (auto& line : readLines("app.log")) {
if (line.find("ERROR") != std::string::npos) {
std::cout << line << std::endl;
}
}
注意事项
- C++20 协程是语言级特性,但标准库仅提供了基础框架(如
std::coroutine_handle),尚未提供现成的高级抽象(如std::generator在 C++23 才正式加入),需要自行实现或使用第三方库 - 协程帧在堆上分配,频繁创建短生命周期的协程可能带来内存开销,应考虑复用协程或使用自定义分配器
final_suspend返回suspend_never时协程帧会自动销毁,返回suspend_always时需要手动销毁,否则会内存泄漏- 协程中抛出异常会触发
unhandled_exception(),默认实现通常调用std::terminate(),生产代码中应妥善处理 - 协程的调试较为困难,建议在开发阶段加入日志追踪挂起和恢复的时机
co_await的 Awaitable 对象必须正确实现三个方法,否则编译错误信息可能晦涩难懂
进阶用法
symmetric transfer 避免栈溢出
在递归协程场景中,await_suspend 返回 std::coroutine_handle 可以实现对称转移,避免无限递归导致的栈溢出:
struct RecursiveAwaiter {
std::coroutine_handle<> next;
bool await_ready() { return false; }
// 返回 coroutine_handle 实现对称转移
// 编译器保证在挂起当前协程后立即恢复目标协程,不增加调用栈深度
std::coroutine_handle<> await_suspend(std::coroutine_handle<>) {
return next; // 直接转移到下一个协程
}
void await_resume() {}
};
协程与线程池结合
#include <queue>
#include <mutex>
#include <condition_variable>
// 简易线程池调度器
class ThreadPool {
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex mtx;
std::condition_variable cv;
bool stop = false;
public:
ThreadPool(size_t threads) {
for (size_t i = 0; i < threads; ++i) {
workers.emplace_back([this]() {
while (true) {
std::function<void()> task;
{
std::unique_lock lock(mtx);
cv.wait(lock, [this]() { return stop || !tasks.empty(); });
if (stop && tasks.empty()) return;
task = std::move(tasks.front());
tasks.pop();
}
task();
}
});
}
}
void submit(std::function<void()> task) {
{
std::unique_lock lock(mtx);
tasks.push(std::move(task));
}
cv.notify_one();
}
};
// 在线程池上恢复协程的 Awaitable
struct ThreadPoolAwaiter {
ThreadPool& pool;
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<> handle) {
pool.submit([handle]() { handle.resume(); });
}
void await_resume() {}
};
使用 C++23 std::generator
C++23 正式引入了 std::generator,无需手动实现 Generator 即可使用:
#include <generator>
// C++23 标准生成器,代码更简洁
std::generator<int> range23(int start, int end) {
for (int i = start; i < end; ++i) {
co_yield i;
}
}
// 支持引用类型,避免拷贝
std::generator<const std::string&> getNames(const std::vector<std::string>& names) {
for (const auto& name : names) {
co_yield name; // 按引用产出,零拷贝
}
}