在 C++20 标准中,协程(coroutines)被正式纳入语言核心,为异步编程提供了更自然、可组合的语义。与传统的回调或 Promise 机制相比,协程允许开发者以同步代码的方式书写异步逻辑,同时保持低延迟和高效资源利用。下面以一个简单的文件读取示例,演示如何在 C++20 中使用协程实现异步 I/O,并结合标准库中的 <filesystem>、<fstream> 以及 std::experimental::generator(如果编译器支持)来完成。
1. 环境准备
- 编译器:gcc 10+、clang 11+、MSVC 19.28+(支持 C++20 协程)
- 语言标准:
-std=c++20 - 需要开启协程支持:
-fcoroutines(gcc/clang)或在 MSVC 中自动开启。
g++ -std=c++20 -fcoroutines -O2 async_file_reader.cpp -o async_file_reader
2. 基本概念回顾
co_await:挂起当前协程,并等待可等待对象完成。co_return:返回协程结果,终止协程。std::future/std::promise:传统异步结果容器。std::async:创建后台线程并返回std::future。- **`std::experimental::generator `**(可选):生成器协程,按需产生值。
3. 异步文件读取实现
下面的实现使用了 std::future 与 std::async 来模拟底层 I/O 线程,协程则用来串联这些异步调用。示例读取一个大文件,逐块读取,并在主线程打印进度。
#include <iostream>
#include <fstream>
#include <string>
#include <future>
#include <thread>
#include <chrono>
#include <filesystem>
namespace fs = std::filesystem;
// 读取文件块的异步函数
std::future<std::string> async_read_block(const std::string& path, std::size_t offset, std::size_t size)
{
return std::async(std::launch::async, [path, offset, size]() {
std::ifstream file(path, std::ios::binary);
if (!file) throw std::runtime_error("无法打开文件: " + path);
file.seekg(offset);
std::string buffer(size, '\0');
file.read(&buffer[0], size);
std::size_t bytes_read = file.gcount();
buffer.resize(bytes_read);
return buffer;
});
}
// 协程包装器:使用 co_await 处理 future
std::future<std::string> co_read_block(const std::string& path, std::size_t offset, std::size_t size)
{
std::future<std::string> fut = async_read_block(path, offset, size);
co_return co_await fut; // 等待异步读取完成
}
// 主协程:逐块读取文件
std::future <void> read_file_in_chunks(const std::string& path, std::size_t chunk_size)
{
std::size_t total_size = fs::file_size(path);
std::size_t offset = 0;
std::size_t chunk_num = 0;
while (offset < total_size)
{
std::size_t remaining = total_size - offset;
std::size_t read_size = std::min(chunk_size, remaining);
std::future<std::string> chunk_fut = co_read_block(path, offset, read_size);
std::string data = co_await chunk_fut; // 挂起协程,等待块读取完成
// 在此处可以对 data 进行处理,例如统计字节、写入另一个文件等
std::cout << "块 " << ++chunk_num << " 已读取 " << data.size() << " 字节。\n";
offset += read_size;
}
std::cout << "文件读取完成,总块数: " << chunk_num << "\n";
co_return;
}
int main()
{
std::string file_path = "big_file.bin";
std::size_t chunk_size = 1024 * 1024; // 1 MB
// 启动主协程
std::future <void> reader_fut = read_file_in_chunks(file_path, chunk_size);
// 主线程可以做其他工作,这里简单等待完成
reader_fut.get(); // 等待协程结束
std::cout << "主线程结束。\n";
return 0;
}
关键点说明
async_read_block:底层使用std::async在独立线程中完成磁盘 I/O。co_read_block:将future包装为可co_await的协程,使代码保持同步风格。read_file_in_chunks:主协程循环读取文件块,使用co_await挂起等待 I/O 完成。co_return:返回值可用于传递协程结果或结束信号。
4. 性能与优化
- 线程池:如果文件非常大或需要高并发读取,建议使用自定义线程池而不是
std::async,以减少线程创建销毁开销。 - 内存映射:
mmap(Linux)或CreateFileMapping(Windows)可进一步提升大文件 I/O 效率。 - 协程池:结合第三方协程框架(如 Boost.Asio、cppcoro)可实现更细粒度的调度。
5. 进一步阅读
- 《C++20 规范中的协程设计》
- 《Boost.Asio 与 C++20 协程的结合》
- 《高性能异步 I/O 设计模式》
以上示例展示了如何利用 C++20 协程与标准库的异步工具实现简洁高效的文件读取。掌握协程的挂起与恢复机制后,许多传统的异步编程难题都能以同步的直观方式解决。祝编码愉快!