**在 C++20 中如何使用协程实现异步 I/O?**

在 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::futurestd::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;
}

关键点说明

  1. async_read_block:底层使用 std::async 在独立线程中完成磁盘 I/O。
  2. co_read_block:将 future 包装为可 co_await 的协程,使代码保持同步风格。
  3. read_file_in_chunks:主协程循环读取文件块,使用 co_await 挂起等待 I/O 完成。
  4. 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 协程与标准库的异步工具实现简洁高效的文件读取。掌握协程的挂起与恢复机制后,许多传统的异步编程难题都能以同步的直观方式解决。祝编码愉快!

发表评论