在C++20中,协程(coroutine)被引入为语言级特性,提供了一种轻量级的异步编程模型。相比传统的线程或回调机制,协程可以让我们用同步风格的代码编写异步逻辑,代码可读性更高,错误更少。下面以实现一个异步文件读取器为例,展示如何使用C++20协程完成从磁盘读取文件并返回内容的功能。
1. 预备知识
- co_await:挂起协程,等待一个“可等待”对象完成。
- co_return:返回协程的结果。
- std::suspend_always / std::suspend_never:用于控制协程的挂起/恢复行为。
- promise_type:协程的承诺类型,决定协程的行为和返回值。
2. 设计思路
- 可等待对象:我们需要一个可等待的包装器,用来封装异步 I/O 操作。这里使用
std::future与std::async的组合来模拟异步读取。 - 协程返回类型:定义一个 `Task ` 模板类,表示一个返回类型为 `T` 的协程任务。它内部会持有 `std::future`。
- 异步读取函数:
async_read_file接受文件路径,返回Task<std::string>。在协程内部,使用co_await等待std::future的完成,并将结果返回。
3. 代码实现
#include <iostream>
#include <fstream>
#include <string>
#include <future>
#include <coroutine>
#include <exception>
// 1. Task <T>:协程任务封装
template<typename T>
struct Task {
struct promise_type {
std::future <T> fut;
T result; // 只在需要返回值时使用
// 进入协程时返回的句柄
auto get_return_object() { return Task{std::move(fut)}; }
// 协程开始时不挂起
std::suspend_never initial_suspend() noexcept { return {}; }
// 协程结束时挂起,等待外部获取结果
std::suspend_always final_suspend() noexcept { return {}; }
// 处理异常
void unhandled_exception() { std::terminate(); }
// 返回值
void return_value(T v) {
result = std::move(v);
// 将结果包装到 future
fut = std::async(std::launch::deferred, [r=std::move(result)]{ return r; });
}
};
std::future <T> fut; // 持有协程的 future
// 阻塞等待结果
T get() { return fut.get(); }
};
// 2. 可等待对象:包装 std::future
struct AwaitableFuture {
std::future<std::string> fut;
bool await_ready() { return fut.wait_for(std::chrono::seconds(0)) == std::future_status::ready; }
void await_suspend(std::coroutine_handle<> h) {
// 在后台线程完成后唤醒协程
std::thread([h, f=&fut]() mutable {
f->wait();
h.resume();
}).detach();
}
std::string await_resume() { return fut.get(); }
};
// 3. 异步文件读取
Task<std::string> async_read_file(const std::string& path) {
// 在后台线程读取文件
std::future<std::string> fileFuture = std::async(std::launch::async, [path]() {
std::ifstream ifs(path, std::ios::binary);
if (!ifs) throw std::runtime_error("Cannot open file");
std::string content((std::istreambuf_iterator <char>(ifs)),
std::istreambuf_iterator <char>());
return content;
});
AwaitableFuture awaitable{ std::move(fileFuture) };
// 挂起,等待文件读取完成
std::string data = co_await awaitable;
co_return data;
}
// 4. 主程序
int main() {
try {
auto task = async_read_file("example.txt");
// 这里可以做其他工作
std::cout << "Doing other work while file is loading...\n";
// 等待结果
std::string content = task.get();
std::cout << "File content (" << content.size() << " bytes):\n";
std::cout << content.substr(0, 200) << "...\n";
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << '\n';
}
return 0;
}
代码说明
- `Task `:包装协程的返回值,通过 `co_return` 将结果放入 `std::future`,外部可通过 `get()` 获取。
AwaitableFuture:将std::future<std::string>变为可等待对象。await_suspend在后台线程完成后恢复协程,避免阻塞主线程。async_read_file:启动异步读取,挂起协程直到读取完成,然后返回字符串内容。
4. 性能与可扩展性
- IO 调度:上述示例使用
std::async,内部实现依赖实现细节,可能使用线程池或单线程 I/O。生产环境可使用更高效的异步 I/O(如io_uring、Boost.Asio 的异步文件 I/O)。 - 错误处理:协程内部若抛出异常,
unhandled_exception会直接终止程序。可以在promise_type中自定义unhandled_exception,将异常包装进std::future,让调用方通过get()捕获。 - 多文件并行:可在同一线程内启动多个
async_read_file,并在主循环中co_await所有结果,利用协程调度实现高并发 I/O。
5. 结语
C++20 的协程为异步编程带来了新的语法糖,使得原本需要回调链的异步 I/O 能以同步代码的直观写法实现。通过以上示例,你可以快速搭建一个基于协程的异步文件读取器,并在此基础上扩展到网络、数据库等多种异步任务。祝你编码愉快!