C++20 之 Coroutines 协程
很多人第一次听到协程会联想到线程。对比项线程(Thread)协程(Coroutine)调度方式操作系统内核调度用户态调度(手动/框架)切换开销高(陷入内核态)极低(仅保存/恢复寄存器)并发数量通常数千级别可达百万级别同步原语需要锁、信号量可用无锁设计资源消耗每个线程 1MB+ 栈空间仅需几 KB协程是用户态的可暂停函数。它在线程内部运行,由程序员或调度框架控制何时暂停、何时恢复,不需要操作系统的介
C++20 之 Coroutines 协程
在 C++20 之前,想要写异步代码,你要么忍受回调地狱,要么手写复杂的状态机,要么依赖 Boost.Coroutine 这样的第三方库。C++20 终于在语言层面引入了协程(Coroutines)——一种可以暂停和恢复的函数。有了它,异步代码可以写成同步的样子,生成器可以像 Python 的
yield一样优雅,任务调度也有了更清晰的抽象。
一、为什么需要协程?
先看看传统异步编程的痛点。
痛点一:回调地狱
异步 I/O 操作(网络请求、文件读写)通常依赖回调:
void fetchUserData(const std::string& userId) {
asyncQuery("SELECT * FROM users WHERE id=" + userId,
[](std::string userData) {
asyncParse(userData, [](User user) {
asyncSaveCache(user, [](bool success) {
asyncNotify(user.email, "Profile updated", []() {
std::cout << "All done!" << std::endl;
});
});
});
});
}
嵌套四层回调,逻辑支离破碎,错误处理无从下手。这就是所谓的 “回调地狱”(Callback Hell)。
痛点二:手写状态机
为了解决回调嵌套,开发者往往把异步操作拆解成状态机:
enum class State { Idle, QuerySent, DataReceived, Parsed, Saved };
struct FetchTask {
State state = State::Idle;
std::string userId;
User user;
void resume() {
switch (state) {
case State::Idle:
sendQuery(userId);
state = State::QuerySent;
break;
case State::QuerySent:
user = parse(data);
state = State::DataReceived;
break;
// ... 还有更多状态
}
}
};
状态机虽然解决了嵌套问题,但代码量暴增、逻辑散落在各个状态分支中,可读性和可维护性都很差。
痛点三:第三方库不统一
Boost.Coroutine、libco、ucontext……各家用法不一,移植成本高。C++ 需要一个语言级别的标准方案。
C++20 的协程,就是这个标准方案。
二、什么是协程?
2.1 用户态线程 vs 协程
很多人第一次听到协程会联想到线程。它们的对比:
| 对比项 | 线程(Thread) | 协程(Coroutine) |
|---|---|---|
| 调度方式 | 操作系统内核调度 | 用户态调度(手动/框架) |
| 切换开销 | 高(陷入内核态) | 极低(仅保存/恢复寄存器) |
| 并发数量 | 通常数千级别 | 可达百万级别 |
| 同步原语 | 需要锁、信号量 | 可用无锁设计 |
| 资源消耗 | 每个线程 1MB+ 栈空间 | 仅需几 KB |
一句话总结:协程是用户态的可暂停函数。它在线程内部运行,由程序员或调度框架控制何时暂停、何时恢复,不需要操作系统的介入。
2.2 协程的本质
协程的核心能力只有两个:
- 暂停(suspend):函数执行到一半可以暂停,把当前状态保存起来
- 恢复(resume):之后可以从暂停点恢复执行,就像从未离开过一样
三、核心关键字
C++20 引入了三个新关键字来标识协程:
co_await — 暂停并等待
auto result = co_await asyncOperation();
协程执行到 co_await 时暂停,等待操作完成后恢复。如果操作已经完成,可以立即恢复;如果没完成,协程会挂起,把控制权交还给调用者。
co_yield — 产出值并暂停
co_yield value; // 等价于 co_await promise.yield_value(value)
常用于生成器场景。协程产出一个值后暂停,等调用者请求下一个值时再恢复。
co_return — 协程返回
co_return result; // 协程结束,返回结果
协程的 “return” 语句。执行到 co_return 时协程结束(销毁),可以通过 promise_type::return_value() 或 return_void() 传递返回值。
重要提示: 只要函数体中出现了
co_await、co_yield或co_return中的任意一个,编译器就会将该函数识别为协程。协程的返回类型不能是void以外的普通类型——必须配合协程框架(自定义类型或标准库的std::coroutine_handle)使用。
四、协程的三大组件
C++20 的协程是一个"零开销抽象框架"——标准库只定义了机制,不提供默认实现。你需要自定义(或使用第三方库提供的)三个核心组件:
4.1 Promise 类型(承诺类型)
Promise 是协程的"管家",负责:
- 创建返回对象:
get_return_object()决定协程返回什么 - 控制初始挂起点:
initial_suspend()返回suspend_always(懒启动)或suspend_never(立即执行) - 控制最终挂起点:
final_suspend()返回的 Awaitable 决定协程结束时是否挂起 - 处理返回值:
return_value(T)或return_void() - 处理
co_yield:yield_value(T) - 处理异常:
unhandled_exception()捕获协程内部未处理的异常
4.2 Awaitable/Awaiter(可等待对象)
当你写 co_await someObj 时,someObj 必须是一个 Awaitable,即提供三个方法:
| 方法 | 作用 |
|---|---|
await_ready() |
如果返回 true,不需要挂起,直接获取结果 |
await_suspend(handle) |
挂起时执行,可以将协程句柄保存起来以便之后恢复 |
await_resume() |
恢复时调用,返回操作的结果 |
一个简单的 awaiter 示例——立即就绪的 awaiter:
struct ReadyAwaiter {
bool await_ready() { return true; } // 不需要挂起
void await_suspend(std::coroutine_handle<>) {} // 无操作
int await_resume() { return 42; } // 直接返回 42
};
4.3 Coroutine Handle(协程句柄)
std::coroutine_handle<> 是协程的"遥控器",可以用来:
resume():恢复协程执行destroy():销毁协程对象并释放栈帧done():检查协程是否已完成
auto handle = ...; // 获取协程句柄
if (!handle.done()) {
handle.resume(); // 恢复执行
}
handle.destroy(); // 销毁协程
⚠️ 注意: Handle 本身不拥有协程的生命周期。你必须在适当的时候手动
destroy(),否则会导致内存泄漏。
五、实战示例
示例 1:简单生成器(Generator)
生成器是最直观的协程用法——按需产出一系列值。
#include <iostream>
#include <coroutine>
#include <optional>
// 简单的 Generator 实现
template<typename T>
class Generator {
public:
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 = std::move(value);
return {};
}
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
// 迭代器支持
struct iterator {
std::coroutine_handle<promise_type> handle;
bool operator!=(std::default_sentinel_t) const {
return !handle.done();
}
const T& operator*() const {
return handle.promise().current_value;
}
iterator& operator++() {
handle.resume();
return *this;
}
};
explicit Generator(std::coroutine_handle<promise_type> h) : handle_(h) {}
~Generator() { if (handle_) handle_.destroy(); }
// 禁止拷贝,允许移动
Generator(const Generator&) = delete;
Generator& operator=(const Generator&) = delete;
Generator(Generator&& other) noexcept : handle_(std::exchange(other.handle_, {})) {}
iterator begin() {
handle_.resume(); // 启动到第一个 yield
return {handle_};
}
std::default_sentinel_t end() { return {}; }
private:
std::coroutine_handle<promise_type> handle_;
};
// 使用生成器
Generator<int> fibonacci() {
int a = 0, b = 1;
while (true) {
co_yield a;
auto temp = a;
a = b;
b = temp + b;
}
}
int main() {
// 生成前 10 个斐波那契数
int count = 0;
for (auto num : fibonacci()) {
std::cout << num << " ";
if (++count >= 10) break;
}
std::cout << std::endl;
// 输出: 0 1 1 2 3 5 8 13 21 34
}
关键点:
initial_suspend()返回suspend_always实现懒启动——协程不会立即执行,等begin()被调用才启动co_yield将值保存到promise_type::current_value,然后挂起final_suspend()也返回suspend_always,协程结束后句柄仍然有效,可以检查done()- 通过
begin()/end()实现 range-based for 循环支持
示例 2:异步任务(Task)
这是最常见的协程应用——把异步操作包装成可以 co_await 的 Task。
#include <iostream>
#include <coroutine>
#include <string>
#include <future>
#include <thread>
#include <chrono>
// 简化的 Task 类型
template<typename T = void>
class Task {
public:
struct promise_type {
T result;
Task get_return_object() {
return Task{
std::coroutine_handle<promise_type>::from_promise(*this)
};
}
std::suspend_always initial_suspend() { return {}; } // 懒启动
std::suspend_always final_suspend() noexcept { return {}; } // 保持帧存活
void return_value(T value) { result = std::move(value); }
void unhandled_exception() { std::terminate(); }
};
explicit Task(std::coroutine_handle<promise_type> h) : handle_(h) {}
~Task() { if (handle_) handle_.destroy(); }
Task(const Task&) = delete;
Task(Task&& other) noexcept : handle_(std::exchange(other.handle_, {})) {}
// 实现 Awaitable 接口,支持 co_await
auto operator co_await() const noexcept {
struct Awaiter {
std::coroutine_handle<promise_type> handle;
bool await_ready() const noexcept { return handle.done(); }
void await_suspend(std::coroutine_handle<> awaiting) noexcept {
handle.resume(); // 简化版:同步恢复
}
T await_resume() { return std::move(handle.promise().result); }
};
return Awaiter{handle_};
}
void resume() { handle_.resume(); }
T get_result() {
return std::move(handle_.promise().result);
}
private:
std::coroutine_handle<promise_type> handle_;
};
// 模拟异步操作
Task<std::string> fetchUserData(const std::string& userId) {
// 模拟网络延迟
std::this_thread::sleep_for(std::chrono::milliseconds(100));
co_return "User{" + userId + ", name: \"LinXi\"}";
}
Task<std::string> fetchUserPosts(const std::string& userId) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
co_return "Posts for " + userId + ": [\"C++20 协程\", \"Ranges\"]";
}
// 协程链式调用:获取用户信息后获取帖子
Task<std::string> buildUserProfile(const std::string& userId) {
std::cout << "[buildUserProfile] 开始获取用户数据..." << std::endl;
auto userData = co_await fetchUserData(userId);
std::cout << "[buildUserProfile] 用户数据: " << userData << std::endl;
auto posts = co_await fetchUserPosts(userId);
std::cout << "[buildUserProfile] 用户帖子: " << posts << std::endl;
co_return userData + " | " + posts;
}
int main() {
auto task = buildUserProfile("u001");
task.resume(); // 启动协程
std::cout << "\n最终结果: " << task.get_result() << std::endl;
}
输出:
[buildUserProfile] 开始获取用户数据...
[buildUserProfile] 用户数据: User{u001, name: "LinXi"}
[buildUserProfile] 用户帖子: Posts for u001: ["C++20 协程", "Ranges"]
最终结果: User{u001, name: "LinXi"} | Posts for u001: ["C++20 协程", "Ranges"]
注意: 这里使用了简化的 Task 实现来展示核心概念。生产环境建议使用成熟的协程库(如 cppcoro、asyncio 等),它们提供了完整的 co_await 支持和真正的异步 I/O 集成。
示例 3:co_await 的工作原理
这个示例深入展示 co_await 的三个阶段:
#include <iostream>
#include <coroutine>
#include <chrono>
// 自定义 Awaiter:延迟指定毫秒后就绪
class DelayAwaiter {
public:
explicit DelayAwaiter(int ms) : milliseconds_(ms) {}
bool await_ready() {
return false; // 总是需要挂起
}
void await_suspend(std::coroutine_handle<> handle) {
std::cout << " [await_suspend] 挂起协程,启动定时器 " << milliseconds_ << "ms" << std::endl;
// 在实际应用中,这里会注册定时器回调
// 定时器到期后调用 handle.resume()
// 这里用 sleep 模拟
std::thread([this, handle]() {
std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds_));
std::cout << " [定时器] " << milliseconds_ << "ms 到期,恢复协程" << std::endl;
handle.resume(); // 恢复协程
}).detach();
}
void await_resume() {
std::cout << " [await_resume] 协程已恢复,继续执行" << std::endl;
}
private:
int milliseconds_;
};
// 简单协程
struct SimpleTask {
struct promise_type {
SimpleTask get_return_object() { return {}; }
std::suspend_never initial_suspend() { return {}; }
std::suspend_never final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
};
SimpleTask simpleCoroutine() {
std::cout << "[协程] 开始执行" << std::endl;
co_await DelayAwaiter(200); // 暂停 200ms
std::cout << "[协程] 恢复后继续执行" << std::endl;
co_await DelayAwaiter(100); // 再暂停 100ms
std::cout << "[协程] 全部完成!" << std::endl;
}
int main() {
std::cout << "=== co_await 工作流程演示 ===" << std::endl;
auto coro = simpleCoroutine();
std::cout << "[主线程] 协程已启动,主线程继续工作" << std::endl;
// 等待协程完成(实际应用中会有事件循环)
std::this_thread::sleep_for(std::chrono::seconds(1));
std::cout << "[主线程] 程序结束" << std::endl;
}
执行流程:
示例 4:异常处理
协程中的异常会被 Promise 捕获,需要妥善处理:
#include <iostream>
#include <coroutine>
#include <stdexcept>
// 带异常处理的 Task
class TaskEx {
public:
struct promise_type {
bool has_exception = false;
std::exception_ptr exception;
TaskEx get_return_object() {
return TaskEx{
std::coroutine_handle<promise_type>::from_promise(*this)
};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {
exception = std::current_exception();
has_exception = true;
}
};
explicit TaskEx(std::coroutine_handle<promise_type> h) : handle_(h) {}
~TaskEx() { if (handle_) handle_.destroy(); }
TaskEx(TaskEx&& other) noexcept : handle_(std::exchange(other.handle_, {})) {}
TaskEx(const TaskEx&) = delete;
void resume() { handle_.resume(); }
void get_result() {
if (handle_.promise().has_exception) {
std::rethrow_exception(handle_.promise().exception);
}
}
private:
std::coroutine_handle<promise_type> handle_;
};
TaskEx riskyOperation() {
std::cout << "执行可能失败的操作..." << std::endl;
bool success = false; // 模拟失败
if (!success) {
throw std::runtime_error("操作失败:网络连接超时");
}
co_return;
}
int main() {
auto task = riskyOperation();
task.resume();
try {
task.get_result();
} catch (const std::exception& e) {
std::cerr << "捕获协程异常: " << e.what() << std::endl;
}
}
六、注意事项
1. 协程的生命周期管理
这是使用协程最容易踩坑的地方:
// ❌ 危险:协程引用了局部变量,协程挂起后变量已销毁
// ❌ 另外,auto 不能用作协程返回类型
SimpleTask badCoroutine() {
int localVar = 42;
co_await someOperation();
// localVar 可能已经不存在了!
}
// ✅ 安全:通过值捕获或使用 shared_ptr,且使用明确的返回类型
Task<int> goodCoroutine(int value) {
auto sharedData = std::make_shared<int>(value);
co_await someOperation();
// sharedData 仍然有效
co_return *sharedData;
}
原则: 协程帧(coroutine frame)中的所有变量在 co_await 暂点之间都必须保持有效。不要捕获引用指向栈上的局部变量。
2. 异常处理
- 协程中未捕获的异常会传播到
promise_type::unhandled_exception() - 如果你没有实现
unhandled_exception(),默认行为是std::terminate() - 最佳实践: 始终实现
unhandled_exception(),至少记录日志
void unhandled_exception() {
// 方式一:存储异常,稍后处理
exception_ = std::current_exception();
// 方式二:终止程序(仅在不可恢复的错误时使用)
// std::terminate();
}
3. 性能考量
协程本身几乎零开销,但需要注意:
| 关注点 | 说明 |
|---|---|
| 协程帧分配 | 每次创建协程会在堆上分配一帧(约几十字节),高频创建时考虑对象池 |
| 挂起/恢复 | 仅保存/恢复少量寄存器,开销约几十纳秒 |
| 编译时间 | 协程的模板展开会增加编译时间 |
| 调试难度 | 协程的执行流不直观,断点调试较困难 |
4. 不要滥用协程
协程不是银弹。适合用协程的场景:
- ✅ 异步 I/O(网络、文件)
- ✅ 生成器/迭代器
- ✅ 任务调度/协程池
- ✅ 游戏中的状态机
不适合的场景:
- ❌ 简单的同步计算(直接写普通函数就好)
- ❌ CPU 密集型并行计算(用
std::thread或 OpenMP) - ❌ 需要简单、直观的控制流时
七、编译器支持
C++20 协程需要较新版本的编译器:
| 编译器 | 最低版本 | 备注 |
|---|---|---|
| GCC | 10.0+ | 基本支持;13+ 较完善 |
| Clang | 14.0+ | 较完善;建议用最新稳定版 |
| MSVC | VS 2019 16.10+ | 建议用 VS 2022 以获得最佳支持 |
编译标志:
# GCC
g++ -std=c++20 -fcoroutines main.cpp -o main
# Clang
clang++ -std=c++20 -stdlib=libc++ main.cpp -o main
# MSVC (VS 2022)
cl /std:c++20 main.cpp
注意: MSVC 默认启用协程支持,不需要额外标志。GCC 需要
-fcoroutines(GCC 10/11)或-fcoroutines-ts。GCC 12+ 以-std=c++20即可。
推荐的第三方库
由于 C++20 标准只提供了协程的框架,没有提供开箱即用的 Task、Generator 等类型,推荐使用以下成熟库:
| 库 | 特点 | 链接 |
|---|---|---|
| cppcoro | 参考实现,功能全面 | GitHub |
| asyncio | 轻量级异步 I/O | GitHub |
总结
C++20 的协程是语言层面的重大革新,它解决了困扰 C++ 社区多年的异步编程难题:
- ✅ 三个新关键字:
co_await(等待)、co_yield(产出)、co_return(返回) - ✅ 三大组件:Promise(承诺)、Awaitable(可等待对象)、Coroutine Handle(句柄)
- ✅ 零运行时开销:协程的挂起/恢复仅需保存/恢复少量寄存器
- ✅ 应用场景广泛:异步编程、生成器、任务调度
- ⚠️ 学习曲线陡峭:理解 Promise 和 Awaitable 需要一些时间投入
- ⚠️ 标准库支持有限:目前需要搭配第三方库使用
协程是 C++20 中最有深度的特性之一,一旦掌握,你会发现异步代码可以写得像同步代码一样优雅。
📌 下一篇预告: C++20 引入的另一个重量级特性——Ranges 库。它带来了函数式风格的 range 管道操作,彻底改变了数据处理的方式。
views::filter、views::transform、views::take……敬请期待!
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)