协程(Coroutines)是C++20中一个重要的新特性,它为编写异步、非阻塞代码提供了更直观、更高效的方式。与传统的回调、Future或线程模型相比,协程通过让函数在执行过程中“挂起”和“恢复”,实现了类似同步代码的可读性,却不需要额外的线程开销。本文将从协程的基本概念、实现原理、标准库支持以及实际应用场景展开详细介绍,并给出完整的代码示例。
1. 协程的基本概念
1.1 什么是协程?
协程是一种在同一线程中多点挂起与恢复的函数。它可以在需要的时候“暂停”执行,保存当前状态,然后在后续再恢复执行。相比普通函数,协程可以在多处返回,而不必一次性返回完整结果。
1.2 与线程的区别
- 线程:每个线程都有独立的栈和调度,创建线程开销大。
- 协程:共享同一线程,栈由协程内部实现管理,开销极低。
- 协程适用于I/O密集型、事件驱动等场景;线程适用于CPU密集型并行计算。
2. C++20协程的实现原理
2.1 关键语法元素
co_await:等待一个异步操作完成。co_yield:产生一个值并挂起协程。co_return:返回协程最终结果并结束协程。
2.2 生成器(Generator)模式
使用 co_yield 可以轻松实现一个生成器。编译器会生成一个内部状态机,管理协程的生命周期和局部变量的保存。
2.3 协程句柄(std::coroutine_handle)
协程句柄用于手动控制协程的挂起与恢复。标准库提供了 std::suspend_always、std::suspend_never 作为默认的挂起策略。
3. 标准库对协程的支持
| 模块 | 功能 | 说明 |
|---|---|---|
std::generator(C++23) |
生成器 | 通过 co_yield 生成一系列值 |
std::task(C++23) |
异步任务 | 以 co_await 为核心 |
std::future |
异步结果 | 与协程结合,支持 co_await |
std::suspend_always / suspend_never |
挂起策略 | 控制挂起与立即继续 |
注意:截至 C++20,生成器等高级功能仅在 C++23 标准中正式加入,C++20 中可使用第三方库(如 cppcoro、boost::asio 等)或手写协程包装器。
4. 实际应用示例
下面以一个简单的异步文件读取为例,演示如何使用协程配合 std::future 实现非阻塞 I/O。代码使用 C++20 标准,假设操作系统提供了异步文件读取 API(如 io_uring 或 ReadFileEx)。
#include <iostream>
#include <coroutine>
#include <future>
#include <cstring>
#include <filesystem>
#include <fstream>
namespace fs = std::filesystem;
// 简单的异步文件读取模拟
struct AsyncReadResult {
std::string data;
std::exception_ptr eptr = nullptr;
};
struct AsyncFileReader {
struct promise_type {
AsyncReadResult result;
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_value(AsyncReadResult r) { result = std::move(r); }
void unhandled_exception() { result.eptr = std::current_exception(); }
};
std::coroutine_handle <promise_type> handle;
AsyncFileReader(std::coroutine_handle <promise_type> h) : handle(h) {}
~AsyncFileReader() { if (handle) handle.destroy(); }
// 用作 co_await 的 awaiter
bool await_ready() const noexcept { return false; }
void await_suspend(std::coroutine_handle<> awaiting) noexcept {
// 模拟异步 I/O: 这里直接使用同步读取并在后台线程完成
std::thread([this, awaiting](){
try {
std::ifstream file;
file.open(handle.promise().result.data); // 数据字段暂时用于文件名
std::stringstream buffer;
buffer << file.rdbuf();
handle.promise().result.data = buffer.str();
} catch (...) {
handle.promise().result.eptr = std::current_exception();
}
awaiting.resume(); // 恢复调用者
}).detach();
}
AsyncReadResult await_resume() {
if (handle.promise().eptr) std::rethrow_exception(handle.promise().eptr);
return std::move(handle.promise().result);
}
};
// 协程函数,读取文件内容
AsyncFileReader read_file_async(const std::string& path) {
AsyncReadResult res;
res.data = path; // 暂存文件名
co_return res;
}
// 主程序
int main() {
try {
std::cout << "开始异步读取文件...\n";
auto reader = read_file_async("sample.txt");
auto result = co_await reader; // 等待协程完成
std::cout << "文件内容长度: " << result.data.size() << " 字节\n";
} catch (const std::exception& e) {
std::cerr << "读取失败: " << e.what() << '\n';
}
}
关键点说明
AsyncFileReader:包装了协程句柄,并实现了await_ready/suspend/resume逻辑。read_file_async:协程函数,返回AsyncFileReader,可以被co_await。- 后台线程:在
await_suspend内部启动异步读取,完成后恢复协程。 - 错误处理:通过
unhandled_exception捕获异常,await_resume再次抛出。
在实际项目中,可将上述逻辑与系统底层的异步 I/O API(如
libuv、boost::asio、io_uring)结合,进一步提升性能。
5. 小结
C++20 的协程特性为现代 C++ 开发提供了更直观、轻量级的异步编程模型。通过掌握 co_await、co_yield、co_return 等关键字,以及标准库的协程支持,开发者可以在保持代码可读性的同时,减少线程切换开销,提升程序的并发性能。未来的 C++23 将进一步完善协程生态,建议关注标准化进展,并尝试在项目中逐步迁移到协程式编程。