协程是 C++20 引入的一项重要特性,它通过 co_await、co_yield 与 co_return 三个关键字,使得函数可以在执行过程中暂停与恢复,从而实现轻量级的异步编程。相比传统的线程、回调和 promise/async,协程具有更低的栈占用、更清晰的业务逻辑写法以及更优的性能。本文将从实现原理、关键接口、内存模型以及实际应用场景四个维度,深入剖析 C++ 协程,并给出一份完整的实战示例。
1. 协程的底层实现
C++ 协程的实现核心是 状态机。当编译器遇到带 co_await/co_yield/co_return 的函数时,会把它拆解成一个隐式生成的类 promise_type(承诺类型)和一个 生成器(coroutine handle)。该类包含所有局部变量的拷贝/移动语义以及 initial_suspend()、final_suspend() 等生命周期钩子。
struct MyPromise {
int value_;
auto get_return_object() { return std::coroutine_handle <MyPromise>::from_promise(*this); }
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() { std::terminate(); }
void return_value(int v) { value_ = v; }
};
当协程执行到 co_await 时,编译器会把控制权交给 awaiter 对象,awaiter 必须实现 await_ready()、await_suspend() 与 await_resume() 三个成员函数。await_suspend 返回 true 时协程被挂起,false 则立即继续。
协程的栈由 resume 栈 与 帧栈 组成:
- 帧栈(Frame)记录协程内部的局部变量以及
promise_type对象。 - resume 栈 存储协程的返回地址,类似于普通函数的返回栈。
因为帧栈被编译器在栈空间之外(通常是堆)分配,所以协程可以在任何调用层级被挂起,甚至跨线程恢复。
2. 关键接口与语义
| 关键字 | 说明 | 典型用法 |
|---|---|---|
co_await |
等待一个 awaiter,返回 awaiter 的 await_resume() 结果 |
auto result = co_await asyncOperation(); |
co_yield |
产生一个值并挂起协程 | co_yield i; |
co_return |
结束协程并返回值 | co_return 42; |
std::future 与协程配合使用时,常见的实现是 std::future::operator co_await。C++20 通过 std::experimental::coroutine_traits 为自定义 awaiter 提供适配接口,允许把任意对象转成 awaiter。
3. 内存模型与异常传播
协程的异常传播与普通函数类似,异常会在 await_suspend 或者 co_return 处被捕获,并交给 promise_type::unhandled_exception() 处理。若你想在协程内部捕获异常,可以直接使用 try/catch 包围 co_await。
协程帧中保存的对象会遵守 RAII 原则,异常导致的堆栈展开会自动析构。值得注意的是,promise_type 必须是 noexcept 的,除非你手动在 unhandled_exception() 里做异常处理。
4. 实战示例:异步文件读取
下面给出一个简易的异步文件读取协程,演示如何结合 std::ifstream、co_await 与 std::future:
#include <coroutine>
#include <future>
#include <fstream>
#include <string>
#include <iostream>
struct AsyncReadAwaiter {
std::string filename_;
std::string result_;
std::coroutine_handle<> handle_;
AsyncReadAwaiter(const std::string& f, std::coroutine_handle<> h)
: filename_(f), handle_(h) {}
bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<> h) noexcept {
std::thread([this, h]{
std::ifstream in(filename_);
if (in) {
std::ostringstream ss;
ss << in.rdbuf();
result_ = ss.str();
}
h.resume(); // 恢复协程
}).detach();
}
const std::string& await_resume() const noexcept { return result_; }
};
std::future<std::string> asyncReadFile(const std::string& file) {
struct Awaiter : AsyncReadAwaiter {
Awaiter(const std::string& f, std::coroutine_handle<> h)
: AsyncReadAwaiter(f, h) {}
};
struct AwaiterPromise {
std::promise<std::string> prom_;
Awaiter get_return_object() { return {prom_.get_future(), std::coroutine_handle<>::from_promise(*this)}; }
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() { prom_.set_exception(std::current_exception()); }
void return_value(std::string v) { prom_.set_value(std::move(v)); }
};
struct AwaiterCoro {
AwaiterCoro(Awaiter&& a) : a_(std::move(a)) {}
Awaiter a_;
auto operator co_await() const noexcept { return a_; }
};
struct Coroutine {
std::coroutine_handle <AwaiterPromise> handle_;
std::future<std::string> get() { return handle_.promise().prom_.get_future(); }
};
auto coro = []() -> AwaiterCoro {
std::string content = co_await Awaiter(file, std::coroutine_handle<>::from_promise(*this));
co_return std::move(content);
}();
return coro.get();
}
int main() {
auto fut = asyncReadFile("sample.txt");
std::cout << "File content:\n" << fut.get() << std::endl;
return 0;
}
说明:
AsyncReadAwaiter在后台线程中读取文件,然后恢复协程。asyncReadFile返回一个std::future<std::string>,主线程可继续执行。- 该示例演示了协程与线程、
std::future的互操作,充分利用协程的非阻塞特性。
5. 性能与最佳实践
| 方面 | 建议 |
|---|---|
| 堆栈开销 | 对于频繁创建的小协程,考虑使用 std::suspend_always 以避免不必要的栈帧。 |
| 异常处理 | 在 co_await 前后使用 try/catch 捕获异常,避免全局崩溃。 |
| 内存池 | 对于大规模协程,可使用自定义 promise_type 的内存池,以减少分配次数。 |
| 任务拆分 | 通过 co_yield 产生子任务,让协程保持轻量,避免单协程阻塞。 |
6. 小结
C++ 协程通过将函数分割成可挂起的状态机,为异步编程提供了一种更接近同步语义的写法。其实现基于 promise_type 与 coroutine_handle,协程的栈不依赖调用栈,能够跨线程挂起与恢复。掌握 co_await 的 awaiter 机制、异常传播与内存模型,能让你在性能与易用性之间取得最佳平衡。希望本文能帮助你快速上手 C++ 协程,并在实际项目中充分发挥其优势。