关于 cpp-coroutines
面向 C++ 开发者的 C++20 协程指南,讲解协程关键字、promise_type、挂起与恢复、帧内存及 GDB 调试。还涵盖生成器、Boost.Asio 协程用法和降低帧分配开销的方法。
主要功能
- 01理解 co_await、co_yield 和 co_return
- 02实现协程类型与 promise_type
- 03创建生成器和 awaitable
- 04检查协程帧布局与内存
- 05在 GDB 中调试挂起的协程
- 06识别常见错误并减少分配开销
适用场景
- 01为异步任务实现 C++20 协程类型
- 02用 co_yield 编写惰性序列生成器
- 03检查协程挂起后的状态和帧内存
- 04排查句柄生命周期与重复恢复问题
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ mohitmishra786/ low-level-dev-skills --skill cpp-coroutines安装后在 Claude Code 中输入/cpp-coroutines确认已加载。
详细介绍
C++20 协程
用途
指导 Agent 了解 C++20 协程机制:co_await、co_yield、co_return,实现必需的 promise_type,理解协程帧的内存布局,在 GDB 中调试挂起的协程,以及减少协程帧分配开销。
触发条件
- “
co_await、co_yield和co_return如何工作?” - “如何为协程实现
promise_type?” - “协程如何挂起和恢复?”
- “如何在 GDB 中调试挂起的协程?”
- “协程帧会占用多少内存?”
- “如何使用
co_yield编写生成器?”
工作流
1. 三个协程关键字
// co_return — return a value and end the coroutine
co_return value;
// co_yield — produce a value, suspend, resume later
co_yield value;
// co_await — suspend until an awaitable completes
auto result = co_await some_awaitable;
如果函数包含这三个关键字中的任意一个,它就是协程。其返回类型必须是带有 promise_type 的协程类型。
2. 最简协程类型 — Task
#include <coroutine>
#include <stdexcept>
#include <optional>
template <typename T>
struct Task {
struct promise_type {
std::optional<T> value;
std::exception_ptr exception;
Task get_return_object() {
return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() { return {}; } // lazy start
std::suspend_always final_suspend() noexcept { return {}; }
void return_value(T v) { value = std::move(v); }
void unhandled_exception() { exception = std::current_exception(); }
};
std::coroutine_handle<promise_type> handle;
explicit Task(std::coroutine_handle<promise_type> h) : handle(h) {}
Task(Task&&) = default;
Task& operator=(Task&&) = default;
~Task() { if (handle) handle.destroy(); }
T get() {
handle.resume(); // resume to completion
if (handle.promise().exception)
std::rethrow_exception(handle.promise().exception);
return std::move(*handle.promise().value);
}
};
// Usage
Task<int> compute() {
co_return 42;
}
int main() {
auto task = compute();
int result = task.get(); // 42
}
3. 使用 co_yield 的生成器
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 {}; }
void return_void() {}
void unhandled_exception() { throw; }
std::suspend_always yield_value(T value) {
current_value = value;
return {}; // suspend after yielding
}
};
std::coroutine_handle<promise_type> handle;
explicit Generator(std::coroutine_handle<promise_type> h) : handle(h) {}
~Generator() { if (handle) handle.destroy(); }
struct iterator {
std::coroutine_handle<promise_type> handle;
bool done;
iterator& operator++() {
handle.resume();
done = handle.done();
return *this;
}
T operator*() const { return handle.promise().current_value; }
bool operator!=(std::default_sentinel_t) const { return !done; }
};
iterator begin() {
handle.resume(); // advance to first yield
return {handle, handle.done()};
}
std::default_sentinel_t end() { return {}; }
};
// Usage
Generator<int> iota(int start, int end) {
for (int i = start; i < end; ++i)
co_yield i;
}
for (int x : iota(0, 5)) {
std::cout << x << ' '; // 0 1 2 3 4
}
4. Awaitable — 自定义 co_await 目标
// An awaitable has three methods:
// await_ready() — true means don't suspend
// await_suspend(handle) — suspend: store handle, schedule resume
// await_resume() — return value of co_await expression
struct TimerAwaitable {
int delay_ms;
bool await_ready() const noexcept { return delay_ms <= 0; }
void await_suspend(std::coroutine_handle<> h) {
// Schedule h.resume() to be called after delay
std::thread([h, this]() {
std::this_thread::sleep_for(std::chrono::milliseconds(delay_ms));
h.resume();
}).detach();
}
void await_resume() const noexcept {} // no return value
};
// suspend_always and suspend_never are built-in awaitables
std::suspend_always{}; // always suspends
std::suspend_never{}; // never suspends (no-op)
5. 协程帧布局和内存
编译器会分配一个协程帧(堆对象),其中包含:
- 在挂起点之间仍然存活的局部变量
- promise 对象
- 当前挂起状态(恢复位置)
- 指向恢复函数和销毁函数的指针
// Inspect frame size with Compiler Explorer (godbolt.org)
// Compile with: g++ -std=c++20 -O2 -S
// Look for: operator new call size in the generated asm
// Or: clang -std=c++20 -O2 -emit-llvm -S | grep "coro.size"
// Reduce frame size:
// 1. Don't keep large objects alive across co_await
struct Bad {
std::vector<char> large_buf; // whole vector lives in frame
co_return large_buf.size(); // large_buf crosses suspension
};
// 2. Move data out before suspending
std::vector<char> buf = get_data();
size_t sz = buf.size(); // capture only what's needed
buf.clear(); // release before suspension
co_await next_event;
// sz still valid; buf released
6. 在 GDB 中调试挂起的协程
# Coroutines appear as regular stack frames after resume()
# To inspect a suspended coroutine:
(gdb) info locals
# Look for coroutine_handle variables
# Print the promise object
(gdb) p *(promise_type*)(handle.__handle_)
# GDB 14+ has coroutine-specific support
(gdb) info coroutines # GCC coroutine support (experimental)
# Step through coroutine execution
(gdb) step # enters co_await implementation
(gdb) finish # returns from coroutine frame function
(gdb) next # step over suspension point
# View all threads (coroutines running on thread pool)
(gdb) info threads
(gdb) thread 2
(gdb) bt
7. Boost.Asio co_spawn 和 co_await
#include <boost/asio.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/awaitable.hpp>
namespace net = boost::asio;
net::awaitable<void> echo_session(net::ip::tcp::socket socket) {
char buf[1024];
for (;;) {
std::size_t n = co_await socket.async_read_some(net::buffer(buf));
co_await net::async_write(socket, net::buffer(buf, n));
}
}
int main() {
net::io_context io;
net::co_spawn(io, listen_accept(io), net::detached);
io.run();
}
co_spawn 在执行器上启动协程;co_await 串联完成令牌,而无需嵌套回调。
8. std::generator(C++23)
#include <generator>
#include <ranges>
std::generator<int> fibonacci() {
int a = 0, b = 1;
while (true) {
co_yield a;
auto next = a + b;
a = b;
b = next;
}
}
// Usage
for (int v : fibonacci() | std::views::take(10))
printf("%d\n", v);
无需手动管理协程句柄的惰性序列 — 编译器会提供 std::generator promise 类型。
9. GDB 中的协程帧布局
# Compile with debug info
g++ -std=c++20 -g -O0 -o app app.cpp
gdb ./app
(gdb) break my_coro
(gdb) run
(gdb) info frame # current stack frame
(gdb) info locals # promise, handle in scope
# Inspect coroutine frame pointer (compiler-specific mangling)
(gdb) p *(MyPromise*)h.address() # h = coroutine_handle
# GCC coroutine support (GCC 14+)
(gdb) info coroutines
# Pretty-print promise state
(gdb) set print pretty on
(gdb) p promise
挂起的协程在恢复前可能不会显示在栈上 — 通过存储的 coroutine_handle 进行跟踪。
10. 对编译时间的影响
协程会增加模板实例化和头文件解析的开销:
| 缓解措施 | 效果 |
|---|---|
-O2 HALO | 减少生成的帧辅助代码 |
在 .cpp 中定义非内联的 co_await | 减少连锁重新编译 |
| 为协程返回类型使用 Pimpl | 避免在头文件中暴露 awaitable 模板 |
ccache / 模块 | 参阅 skills/rust/rust-build-times 中适用于 C++ 的模式 |
使用 g++ -ftime-report 或 clang -ftime-trace 进行测量。协程密集型头文件(如 Asio)可以适度利用统一构建 — 但要权衡 RAM 用量。
11. 常见问题
| 问题 | 原因 | 修复方法 |
|---|---|---|
在非协程中使用 co_await | 函数缺少协程返回类型 | 将返回类型改为协程类型 |
co_return 后句柄悬空 | 协程结束后仍使用句柄 | 恢复前检查 handle.done() |
| 重复恢复 | 恢复已恢复的协程 | 跟踪状态;仅在挂起时恢复 |
| 协程帧从未释放 | 忘记调用 handle.destroy() | 使用 RAII 封装器(Task、Generator) |
| 堆分配开销 | 每次调用协程都会创建新帧 | 使用 -O2 启用 HALO(堆分配消除优化) |
co_await 递归深度过大 | 深层调用链导致栈溢出 | 使用 std::coroutine_handle<> 尾调用模式 |
相关技能
- 其他高级 C++20 特性请参阅
skills/compilers/cpp-templates - Rust 中对应的 Future/Poll 模型请参阅
skills/rust/rust-async-internals - GDB 会话管理请参阅
skills/debuggers/gdb
评论
支持 Markdown