C++20 标准首次正式引入协程(coroutines),为异步编程提供了语言层面的支持。与传统的基于线程或回调的异步模型相比,协程更直观、可组合且性能更优。本文从协程的底层实现原理入手,结合实际代码示例,帮助读者快速掌握协程的使用与注意事项。
一、协程概览
- 协程是一种可以挂起与恢复的轻量级执行单元。编译器将协程拆分为若干“状态点”,每次挂起时保存当前执行状态(包括栈帧),下一次恢复时从上一次挂起点继续执行。
- 语法层面,C++20 对协程的支持主要体现在
co_await、co_yield、co_return等关键字,以及协程返回类型(std::future、std::generator等)上。 - 与线程相比,协程是“协作式”调度,必须显式挂起和恢复;这使得它的上下文切换成本极低,但也需要更严谨的设计。
二、底层实现细节
-
生成器状态机
编译器把协程函数编译成一个生成器对象,该对象内部维护一个“状态机”以及相关的数据成员。每个co_await或co_yield位置对应一个状态值,函数在返回时记录当前状态。 -
Suspend 与 Resume
co_await的实现是await_suspend与await_resume。当协程遇到co_await时,会调用 awaiter 对象的await_suspend,该函数决定是否挂起协程。若挂起,协程的上下文被保存;若不挂起,则继续往下执行。co_yield用于生成器(如std::generator),协程在co_yield时挂起并返回一个值给调用者,随后在下次调用next()时恢复。
-
内存管理
协程的栈不再由系统栈管理,而是由编译器分配在堆上。协程对象中包含一个可变大小的“协程 frame”,存放局部变量、参数和返回地址。栈的分配/释放由operator new/delete处理,使用std::experimental::coroutine_handle进行控制。 -
异常传播
协程可以像普通函数一样抛出异常。异常会在协程的await_suspend或co_return期间传播。若协程返回std::future,异常会包装在 future 中;若返回std::generator,异常会在迭代过程中抛出。
三、实际示例:异步文件读取
#include <iostream>
#include <fstream>
#include <experimental/coroutine>
#include <string>
#include <future>
namespace stdex = std::experimental;
// 简单 awaiter,用于异步读取文件
struct FileReadAwaiter {
std::ifstream& stream;
std::string buffer;
std::size_t bytes_to_read;
bool await_ready() const noexcept { return !stream; } // 如果文件未打开则不挂起
void await_suspend(std::coroutine_handle<> h) const noexcept {
// 在后台线程中读取文件
std::async(std::launch::async, [this, h]() mutable {
buffer.resize(bytes_to_read);
stream.read(&buffer[0], bytes_to_read);
h.resume(); // 读取完成后恢复协程
});
}
std::string await_resume() noexcept { return std::move(buffer); }
};
struct AsyncFileReader {
struct promise_type {
AsyncFileReader get_return_object() {
return AsyncFileReader{ 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() { std::terminate(); }
};
std::coroutine_handle <promise_type> coro;
AsyncFileReader(std::coroutine_handle <promise_type> h) : coro(h) {}
~AsyncFileReader() { if (coro) coro.destroy(); }
std::future<std::string> read(std::ifstream& stream, std::size_t size) {
struct Awaiter {
std::ifstream& stream;
std::size_t size;
std::future<std::string> fut;
Awaiter(std::ifstream& s, std::size_t sz)
: stream(s), size(sz), fut(std::async(std::launch::async, []() { return std::string(); })) {}
bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<> h) {
std::async(std::launch::async, [this, h]() {
auto data = FileReadAwaiter{ stream, "", size };
std::string res = co_await data;
fut.get_future().set_value(std::move(res));
h.resume();
});
}
std::string await_resume() noexcept { return fut.get_future().get(); }
};
return std::async(std::launch::async, [this, &stream, size]() -> std::string {
co_await Awaiter{ stream, size };
});
}
};
int main() {
std::ifstream file("sample.txt", std::ios::binary);
if (!file) {
std::cerr << "Cannot open file!\n";
return 1;
}
AsyncFileReader reader{};
auto future = reader.read(file, 1024);
std::string data = future.get();
std::cout << "Read data: " << data.substr(0, 100) << "...\n";
return 0;
}
说明
FileReadAwaiter在后台线程完成文件读取后恢复协程。AsyncFileReader封装了协程对象,提供read方法返回std::future<std::string>。main演示如何启动协程并获取结果。
四、使用建议
- 避免过度嵌套:每层协程都涉及状态机的生成与上下文切换,嵌套太深会导致可读性和性能下降。
- 尽量使用
co_await:将耗时操作封装为 awaiter,保持主协程逻辑的简洁。 - 异常处理:通过
std::future::get()或try-catch捕获协程中的异常。 - 调试工具:IDE 的调试器尚未完全支持协程,但可通过打印日志或使用
std::experimental::suspend_always断点来跟踪执行。
五、总结 C++20 的协程为异步编程提供了一种更自然、更高效的语义。理解其实现细节——状态机、挂起/恢复、协程 frame 的堆分配——有助于编写更可靠、更可维护的协程代码。随着标准化和生态完善,协程将在网络 I/O、游戏开发、嵌入式系统等领域得到更广泛的应用。