C++20 协程(Coroutines)在异步 IO 中的实战指南

协程是 C++20 新增的语言特性,允许我们以“暂停和恢复”的方式编写异步代码,从而使代码更加顺序化、易读且高效。本文将带你快速掌握协程的核心概念,并演示如何利用它实现一个简易的异步文件读取器。

1. 协程基础

协程在 C++ 中由 co_awaitco_yieldco_return 三个关键字实现。它们对应的功能分别是:

  • co_await:等待一个可等待对象(awaitable)的完成,并在完成后继续执行。
  • co_yield:生成一个值并暂停协程,等待下一个 co_yieldco_return
  • co_return:结束协程,并返回最终结果。

要声明一个协程函数,需要返回一个“协程类型”。最常见的两种协程类型是:

  • `std::future `:传统的异步结果容器,兼容 “ 库。
  • `std::generator `(来自 “ 或第三方实现):返回可迭代的值序列。

2. Awaitable 对象

协程需要等待的对象必须满足 Awaitable 协议,即拥有 await_ready()await_suspend()await_resume() 成员函数。标准库提供了一些常用的 Awaitable,例如:

  • `std::future ` 的 `co_await` 会在 future 完成时恢复协程。
  • std::experimental::coroutine_handle:低层次的协程句柄,可用于自定义 Awaitable。

3. 简易异步文件读取

下面演示如何用协程实现一个异步文件读取器。我们使用标准库的 `

` 读取文件,并用 `std::async` 与 `co_await` 配合模拟异步行为。 “`cpp #include #include #include #include #include #include #include #include namespace async_file { struct AwaitableRead { std::ifstream& stream; std::string buffer; std::size_t bytes_to_read; bool await_ready() { return false; } // 总是需要挂起 void await_suspend(std::coroutine_handle h) { std::thread([=]() mutable { // 模拟 I/O 延迟 std::this_thread::sleep_for(std::chrono::milliseconds(100)); buffer.resize(bytes_to_read); stream.read(buffer.data(), bytes_to_read); h.resume(); // 恢复协程 }).detach(); } std::string await_resume() { return buffer; } }; template struct AwaitableFuture { std::future fut; AwaitableFuture(std::future f) : fut(std::move(f)) {} bool await_ready() const noexcept { return fut.wait_for(std::chrono::seconds(0)) == std::future_status::ready; } void await_suspend(std::coroutine_handle h) { std::thread([=]() mutable { fut.wait(); h.resume(); }).detach(); } T await_resume() { return fut.get(); } }; } // namespace async_file // 协程函数:读取文件并返回内容 auto async_read_file(const std::string& path, std::size_t chunk_size = 1024) -> std::future { std::ifstream file(path, std::ios::binary); if (!file) throw std::runtime_error(“Cannot open file”); std::string content; while (file.peek() != EOF) { async_file::AwaitableRead ar{file, “”, chunk_size}; std::string chunk = co_await ar; content += chunk; } co_return content; } int main() { try { auto fut = async_read_file(“sample.txt”); // 在主线程可以做其他工作 std::cout << "Reading file asynchronously…\n"; std::string data = fut.get(); // 阻塞直到文件读取完成 std::cout << "File size: " << data.size() << " bytes\n"; } catch (const std::exception& e) { std::cerr << "Error: " << e.what() << '\n'; } } “` ### 关键点说明 1. **AwaitableRead** – `await_ready()` 总返回 `false`,表示协程始终挂起。 – `await_suspend()` 在独立线程中执行文件读取,完成后调用 `h.resume()` 重新调度协程。 – `await_resume()` 返回读取到的缓冲区。 2. **async_read_file** – 通过 `co_await` 等待 `AwaitableRead` 的完成,将每次读取的块追加到 `content`。 – 最终用 `co_return` 返回完整文件内容。 3. **异步等待** – `std::future` 作为协程返回类型,调用者可以在 `fut.get()` 时等待协程完成,或者使用 `co_await` 在另一个协程中等待。 ## 4. 性能与局限 – **线程数**:上述实现为每个 I/O 操作创建一个线程,适合 I/O 密集型但线程数不多的场景。生产环境建议使用线程池或异步 I/O API(如 `io_uring`、`Boost.Asio`)来替代。 – **错误处理**:在协程内部抛出的异常会自动传递到返回的 `std::future`,在 `get()` 时会抛出。 – **编译器支持**:C++20 协程已在 GCC 10、Clang 12 及 MSVC 19.28 开始支持,但不同编译器的实现细节略有差异,建议使用 `-fcoroutines` 或相应标志。 ## 5. 进一步阅读 – 《C++20 协程深度剖析》 – 《Boost.Asio 与 C++20 协程的结合》 – 《现代 C++:使用 std::generator 进行流式数据处理》 通过本例,你可以看到协程让异步编程变得像同步一样直观。掌握了协程后,可以将其应用到网络请求、数据库查询、文件系统操作等多种 I/O 场景,从而显著提升代码可读性与维护性。

发表评论