C++20 引入的协程(Coroutines)为异步编程带来了极大的便利。它们可以让我们像编写同步代码那样写异步逻辑,隐藏了复杂的状态机实现。下面我们用一个小示例来演示如何利用协程读取文件内容,并把读取结果返回给调用者。
1. 协程的基本概念
协程的核心是一个 promise 对象,它保存协程的状态,并定义协程的入口、挂起点以及结束点。C++ 标准库提供了 std::suspend_always、std::suspend_never 等简易挂起策略,结合 co_await、co_yield 与 co_return,就能实现异步流程。
2. 设计思路
- 异步读取:我们把文件读取封装成一个
async_read协程,读取一个文件块后co_await一个 I/O 事件,完成后返回读取到的字节数。 - 任务包装:使用
std::future来包装协程的最终结果,方便与普通同步代码交互。 - 简易 I/O 事件:由于标准库暂不直接提供事件循环,我们在示例中使用
std::async作为异步 I/O 的占位实现,真正项目中可替换为 libuv、asio 等事件驱动框架。
3. 示例代码
#include <iostream>
#include <fstream>
#include <string>
#include <future>
#include <coroutine>
#include <vector>
#include <thread>
#include <chrono>
// 简单的异步读取事件占位,实际项目请使用 libuv/Asio 等
struct AsyncReadEvent {
struct promise_type {
std::future<std::size_t> get_return_object() {
return std::future<std::size_t>(std::move(result_promise.get_future()));
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_value(std::size_t val) { result_promise.set_value(val); }
void unhandled_exception() { result_promise.set_exception(std::current_exception()); }
std::promise<std::size_t> result_promise;
};
};
using awaitable_size_t = std::future<std::size_t>;
awaitable_size_t async_read_chunk(std::ifstream &ifs, char *buffer, std::size_t size) {
// 这里用 std::async 模拟异步 I/O
return std::async(std::launch::async, [&ifs, buffer, size]() {
std::this_thread::sleep_for(std::chrono::milliseconds(10)); // 模拟 I/O 延迟
ifs.read(buffer, size);
return static_cast<std::size_t>(ifs.gcount());
});
}
struct FileReader {
std::ifstream ifs;
std::size_t chunk_size;
FileReader(const std::string &path, std::size_t chunk = 1024)
: ifs(path, std::ios::binary), chunk_size(chunk) {}
// 协程函数,返回 std::future<std::vector<char>>,包含完整文件内容
std::future<std::vector<char>> read_all() {
std::vector <char> result;
while (ifs) {
std::vector <char> buffer(chunk_size);
auto size_future = async_read_chunk(ifs, buffer.data(), buffer.size());
std::size_t n = co_await size_future; // 等待 I/O 完成
if (n > 0) {
result.insert(result.end(), buffer.begin(), buffer.begin() + n);
}
}
co_return result; // 传回完整文件
}
};
int main() {
FileReader reader("example.txt", 512);
auto future = reader.read_all(); // 启动协程
std::vector <char> data = future.get(); // 阻塞等待结果
std::cout << "读取文件共 " << data.size() << " 字节。\n";
std::cout << "内容预览:\n" << std::string(data.begin(), data.end()).substr(0, 100) << "...\n";
return 0;
}
4. 关键点剖析
-
async_read_chunk
通过std::async模拟异步 I/O。协程在co_await时会挂起,等到async任务完成后恢复执行。 -
协程返回
std::future
std::future让协程的结果可以像普通异步操作一样被等待。若想在事件循环中直接挂起而不阻塞,可以结合自定义事件循环,将awaitable_size_t换成与循环兼容的 awaitable。 -
错误处理
promise_type::unhandled_exception会捕获异常并传递给future,调用方可以通过future.get()捕获异常或检查future.wait_for的状态。 -
可扩展性
- 可以把
async_read_chunk替换为真正的异步文件 I/O,例如使用boost::asio::async_read。 - 对于大文件,建议使用
std::shared_ptr<std::vector<char>>或std::unique_ptr以避免拷贝。 - 结合
std::generator(C++23)可以实现更细粒度的流式读取。
- 可以把
5. 结语
C++20 的协程为 I/O 密集型应用提供了新的思路。通过把异步读取包装成协程,我们既保留了直观的代码风格,又能充分利用异步事件循环的性能。希望这个小示例能为你在项目中使用协程提供参考。祝编码愉快!