一、背景与意义
在传统的 C++ 中,异步 I/O 需要借助线程、回调或第三方库(如 Boost.Asio、libuv 等)。这些方案虽然成熟,但代码往往繁琐、难以维护。C++20 引入了 协程(coroutine),为编写异步逻辑提供了更直观的语言级支持。通过 co_await、co_return 等关键字,开发者可以像同步代码一样书写异步逻辑,从而显著降低复杂度。
本文将以 异步文件读取 为例,演示如何使用 C++20 协程实现一个简洁、可复用的异步读文件 API,并对其内部机制做简要说明。
二、协程基础
在 C++20 中,协程的核心是一个 promise type(承诺类型)与 future type(期货类型)的配合。简化步骤如下:
- 编写 promise type:实现
get_return_object(),initial_suspend(),final_suspend(),return_value(),unhandled_exception()等成员函数。 - 编写 awaitable type:实现
await_ready(),await_suspend(),await_resume()。 - 调用协程:使用
co_await或co_return,得到的对象即为未来的值。
标准库中已提供 std::future, std::promise, std::async 等,但这些都不支持协程。我们可以借助 std::experimental::generator 或第三方库 cppcoro、asio::awaitable 等实现更通用的协程接口。为简化演示,本文自行实现一个轻量级 async_task 类型。
三、实现步骤
1. 定义 async_task
#include <coroutine>
#include <exception>
#include <iostream>
#include <vector>
#include <cstring> // for std::memcpy
// 简易的异步任务包装器
template<typename T>
class async_task {
public:
struct promise_type;
using handle_type = std::coroutine_handle <promise_type>;
async_task(handle_type h) : coro(h) {}
async_task(const async_task&) = delete;
async_task(async_task&& other) noexcept : coro(other.coro) { other.coro = nullptr; }
~async_task() { if (coro) coro.destroy(); }
T get() {
if (!coro.done()) coro.resume();
if (coro.promise().exception) std::rethrow_exception(coro.promise().exception);
return std::move(coro.promise().value);
}
struct promise_type {
T value;
std::exception_ptr exception;
auto get_return_object() {
return async_task{handle_type::from_promise(*this)};
}
std::suspend_never initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void unhandled_exception() { exception = std::current_exception(); }
void return_value(T val) { value = std::move(val); }
};
private:
handle_type coro;
};
说明
promise_type保存返回值与异常。get()方法阻塞等待协程完成,简化使用。- 这里使用
std::suspend_never表示协程立即开始执行,std::suspend_always表示在final_suspend时暂停,让外部释放资源。
2. 定义 awaitable 类型:file_read_op
#include <filesystem>
#include <fcntl.h> // open
#include <unistd.h> // read, close
#include <system_error>
struct file_read_op {
int fd;
std::size_t size;
char* buffer;
file_read_op(int fd, std::size_t size, char* buffer)
: fd(fd), size(size), buffer(buffer) {}
bool await_ready() noexcept { return false; }
// 将协程挂起,并在后台线程完成 I/O 后唤醒
std::suspend_always await_suspend(std::coroutine_handle<> h) noexcept {
// 简单示例:在当前线程同步完成 I/O 并唤醒
// 实际应用可改为线程池或异步 I/O
ssize_t n = ::read(fd, buffer, size);
if (n < 0) {
// 这里我们直接把错误信息写入 promise,省略细节
h.promise().exception = std::make_exception_ptr(
std::system_error(errno, std::generic_category(), "read error"));
} else {
h.promise().value = static_cast<std::size_t>(n);
}
return {}; // 立即唤醒
}
std::size_t await_resume() noexcept { return 0; } // 结果已写入 promise
};
说明
await_suspend在这里直接执行read(),但在真正的异步环境下,你可以把 I/O 操作交给线程池或事件循环。- 我们把读取到的字节数写入协程的
promise对象,使async_task的get()可以获取结果。
3. 组合协程函数
async_task<std::size_t> async_read_file(const std::string& path, std::vector<char>& out) {
int fd = ::open(path.c_str(), O_RDONLY);
if (fd < 0) {
co_return static_cast<std::size_t>(0);
}
// 先获取文件大小
std::filesystem::path p(path);
std::size_t file_size = std::filesystem::file_size(p);
out.resize(file_size);
// 调用 awaitable
co_await file_read_op(fd, file_size, out.data());
::close(fd);
co_return file_size;
}
说明
- 这里把文件大小读取、缓冲区分配、协程等待等逻辑串联。
co_await会挂起当前协程,直到await_suspend完成。
四、使用示例
int main() {
std::vector <char> data;
async_task<std::size_t> task = async_read_file("example.txt", data);
// 可以在此处执行其他同步操作
std::cout << "正在读取文件...\n";
std::size_t bytes = task.get(); // 阻塞等待完成
std::cout << "读取完成,字节数:" << bytes << '\n';
// 输出文件内容
std::cout.write(data.data(), bytes);
std::cout << '\n';
}
五、性能与可扩展性
-
性能
- 协程本身几乎无运行时开销;真正的 I/O 仍是阻塞
read()。 - 若改为事件驱动的异步 I/O(如
io_uring或libuv),只需在await_suspend中注册回调,协程仍然保持简洁。
- 协程本身几乎无运行时开销;真正的 I/O 仍是阻塞
-
可扩展性
async_task可进一步包装为async_task<std::vector<char>>,返回完整缓冲区。- 对于多文件并行读取,可使用
std::vector<async_task<std::size_t>>并调用co_await或std::when_all(第三方库提供)等待全部完成。
-
错误处理
- 在
await_suspend中捕获异常并存入promise,get()会重新抛出。 - 对于 I/O 超时或取消等高级场景,可在
await_suspend中使用std::condition_variable或事件循环的取消机制。
- 在
六、总结
本文展示了如何利用 C++20 协程实现一个简洁的异步文件读取 API。通过自定义 async_task 与 awaitable,我们在保持代码可读性的同时,仍能充分利用系统的异步 I/O 机制。随着 C++20 的普及,协程将成为处理 I/O 密集型任务的首选手段,为高性能、低耦合的应用程序奠定基础。