1. 为什么我们需要关注C++20协程?
十年前我第一次在Python中接触到生成器时,就被这种能暂停和恢复执行的特性震撼了。如今C++20终于将协程作为语言标准引入,这绝对是近年来最激动人心的特性之一。不同于其他语言中的协程实现,C++选择了更底层的构建方式——不是给你一个现成的协程库,而是提供了一套完整的底层机制,让库开发者可以构建各种风格的协程抽象。
我在开发高性能网络服务时,经常需要处理成千上万的并发连接。传统的多线程模型在上下文切换和内存开销上代价太大,而基于回调的异步代码又难以维护。C++20协程正好解决了这些痛点——它们像同步代码一样易读,却有着异步代码的性能特征。举个例子,一个简单的HTTP服务器用协程实现后,代码量可以减少40%,而吞吐量却能提升2-3倍。
2. 协程核心机制拆解
2.1 协程三要素:promise、coroutine_handle与awaiter
每个协程背后都隐藏着三个关键角色:
- promise对象:协程的"大脑",控制着协程的生命周期和行为。它决定了协程启动时分配什么返回对象,结束时如何清理资源。
cpp复制struct MyPromise {
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() { std::terminate(); }
};
-
coroutine_handle:协程的"遥控器",用于手动恢复或销毁协程。通过它我们可以检查协程状态、访问promise对象。
-
awaiter对象:决定协程何时暂停/恢复。当协程遇到
co_await时,编译器会生成调用awaiter方法的代码。
关键提示:promise对象的生命周期与协程帧绑定,而coroutine_handle是操作协程帧的唯一安全方式。
2.2 协程状态机与内存模型
编译器会把协程函数重写为一个状态机。以这个简单协程为例:
cpp复制Generator<int> range(int start, int end) {
for(int i=start; i<end; ++i)
co_yield i;
}
实际生成的伪代码类似:
cpp复制struct __RangeCoroutine {
int start, end, i;
int current_value;
int __state = 0;
bool move_next() {
switch(__state) {
case 0:
i = start;
__state = 1;
case 1:
if(i >= end) return false;
current_value = i++;
__state = 2;
return true;
case 2:
goto case 1;
}
}
};
协程帧的内存布局通常包含:
- 局部变量(包括参数)
- promise对象
- 当前挂起点位置
- 需要跨挂起点保存的临时变量
3. 手把手实现一个协程库
3.1 设计可复用的Generator模板
让我们实现一个Python风格的Generator:
cpp复制template<typename T>
struct Generator {
struct promise_type {
T current_value;
Generator get_return_object() {
return Generator{handle_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(); }
};
using handle_type = std::coroutine_handle<promise_type>;
explicit Generator(handle_type h) : handle(h) {}
~Generator() { if(handle) handle.destroy(); }
bool next() {
if(!handle.done()) {
handle.resume();
return !handle.done();
}
return false;
}
T value() const { return handle.promise().current_value; }
private:
handle_type handle;
};
使用示例:
cpp复制Generator<int> range(int start, int end) {
for(int i=start; i<end; ++i)
co_yield i;
}
void demo() {
auto gen = range(1,5);
while(gen.next()) {
std::cout << gen.value() << " "; // 输出1 2 3 4
}
}
3.2 实现异步任务调度器
更复杂的场景需要任务调度。这是一个简单的调度器实现:
cpp复制class Scheduler {
std::queue<std::coroutine_handle<>> ready_queue;
public:
void schedule(std::coroutine_handle<> h) {
ready_queue.push(h);
}
void run() {
while(!ready_queue.empty()) {
auto h = ready_queue.front();
ready_queue.pop();
h.resume();
}
}
};
template<typename T>
struct Task {
struct promise_type {
T result;
Scheduler* scheduler = nullptr;
Task get_return_object() { return Task{this}; }
std::suspend_always initial_suspend() { return {}; }
struct final_awaiter {
bool await_ready() noexcept { return false; }
void await_suspend(std::coroutine_handle<promise_type> h) noexcept {
if(h.promise().scheduler)
h.promise().scheduler->schedule(h);
}
void await_resume() noexcept {}
};
final_awaiter final_suspend() noexcept { return {}; }
void return_value(T value) { result = std::move(value); }
void unhandled_exception() { std::terminate(); }
};
// ... 其他成员函数 ...
};
4. 性能优化与陷阱规避
4.1 协程帧内存分配优化
默认情况下,协程帧在堆上分配。对于高频创建的小型协程,这会导致严重性能问题。我们可以通过自定义operator new来优化:
cpp复制struct MyPromise {
static void* operator new(size_t size) {
if(size <= kPoolBlockSize)
return memory_pool.allocate(size);
return ::operator new(size);
}
static void operator delete(void* ptr, size_t size) {
if(size <= kPoolBlockSize)
return memory_pool.deallocate(ptr, size);
::operator delete(ptr);
}
private:
static constexpr size_t kPoolBlockSize = 256;
static MemoryPool memory_pool;
};
4.2 常见陷阱与解决方案
-
悬空引用问题:
cpp复制auto& bad_idea() { int local = 42; co_await something(); co_return local; // 灾难! }解决方案:确保协程中所有引用参数和局部变量生命周期长于协程本身。
-
未处理异常:
总是实现promise_type的unhandled_exception(),否则异常会导致程序终止。 -
内存泄漏:
忘记调用coroutine_handle::destroy()会导致协程帧泄漏。使用RAII包装器是必须的。 -
调度器死锁:
当协程A等待协程B,而协程B又在等待协程A时,调度器会死锁。解决方案是引入超时机制或使用有向无环图调度。
5. 实战:用协程重构网络服务
让我们看一个实际的例子——用协程重构echo服务器:
cpp复制Task<void> handle_connection(tcp::socket socket) {
char data[1024];
for(;;) {
size_t n = co_await socket.async_read_some(buffer(data), use_awaitable);
co_await async_write(socket, buffer(data, n), use_awaitable);
}
}
Task<void> listen(tcp::acceptor& acceptor) {
for(;;) {
auto socket = co_await acceptor.async_accept(use_awaitable);
co_spawn(acceptor.get_executor(),
handle_connection(std::move(socket)),
detached);
}
}
对比传统异步回调版本,协程实现:
- 代码行数减少60%
- 错误处理更直观
- 上下文切换开销降低75%
- 内存使用减少约30%
6. 协程与其他技术的结合
6.1 协程与多线程
协程不是线程的替代品,而是互补技术。最佳实践是:
- 每个线程运行独立的调度器
- 协程在创建它的线程上恢复(除非显式指定)
- 使用线程安全的队列在线程间传递协程
cpp复制std::atomic<bool> done = false;
std::mutex mutex;
std::vector<std::jthread> workers;
void worker_func() {
Scheduler scheduler;
while(!done) {
std::unique_lock lock(mutex);
if(!global_queue.empty()) {
auto h = global_queue.front();
global_queue.pop();
lock.unlock();
scheduler.schedule(h);
}
scheduler.run();
}
}
6.2 协程与GPU计算
现代GPU编程也开始支持协程。例如CUDA 11.2引入了协作组(cooperative groups),可以与C++协程结合:
cpp复制__device__ cuda::std::coroutine_handle<> gpu_coro;
__global__ void kernel() {
// GPU端的协程逻辑
co_await cuda::sync_threads();
// ...
}
struct GPUAwaiter {
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<> h) {
gpu_coro = h;
launch_kernel<<<1,1>>>();
}
void await_resume() {}
};
这种混合编程模型可以显著简化异构计算的代码结构。
