**C++20协程:从同步到异步的优雅过渡**

C++20引入了协程(coroutines)作为语言级特性,极大简化了异步编程的实现。相比传统的回调、线程或状态机,协程以“直观且可组合”的方式表达异步流程。本文将从协程的基本概念、实现原理,到实际使用示例(异步文件读取)展开说明,并给出最佳实践与常见坑。


一、协程的基本概念

关键词 含义
协程 可以在任意位置挂起并在未来恢复的函数。
co_await 表示等待一个协程返回,挂起当前协程直到等待对象完成。
co_yield 产生一个值并挂起,支持生成器模式。
co_return 结束协程,返回最终结果。
awaitable 对象必须实现 await_ready(), await_suspend(), await_resume() 三个接口,才能被 co_await

协程在编译时会被展开为普通的状态机,await_suspend 用于决定是否挂起;若挂起,协程会在外部事件完成后被恢复。


二、实现原理概览

  1. 生成状态机:编译器把 co_await / co_yield 的位置生成状态机状态。每一次挂起,状态机将当前局部变量保存在堆上或在寄存器中。
  2. promise_type:每个协程都有对应的 promise_type,负责:
    • 返回值get_return_object() 产生协程句柄。
    • 异常处理unhandled_exception()
    • 挂起/恢复initial_suspend()final_suspend()
  3. 协程句柄:`std::coroutine_handle

    ` 可以用来手动恢复、检查状态或销毁协程。


三、示例:异步文件读取

以下示例演示如何使用协程与 asio 结合实现异步读取文件内容。asio 提供了对 I/O 事件的 awaitable 包装。

#include <iostream>
#include <asio.hpp>
#include <fstream>
#include <string>
#include <vector>

namespace asio = boost::asio;

// 把标准文件流包装成 awaitable
asio::awaitable<std::string> async_read_file(asio::io_context& ctx, const std::string& path) {
    using namespace std::literals;

    // 异步打开文件
    std::error_code ec;
    std::fstream file(path, std::ios::binary | std::ios::in);
    if (!file) {
        co_return "";  // 失败返回空字符串
    }

    // 读取文件内容
    std::vector <char> buffer((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
    std::string content(buffer.begin(), buffer.end());

    co_return content;
}

// 主协程:并发读取两个文件
asio::awaitable <void> main_coro(asio::io_context& ctx) {
    std::string content1 = co_await async_read_file(ctx, "file1.txt");
    std::string content2 = co_await async_read_file(ctx, "file2.txt");

    std::cout << "File1 size: " << content1.size() << " bytes\n";
    std::cout << "File2 size: " << content2.size() << " bytes\n";
}

int main() {
    asio::io_context ctx;

    // 启动协程
    std::make_shared<asio::spawn_handler>(ctx, [](asio::coroutine_handle<> h) {
        main_coro(ctx).resume();
    });

    ctx.run();
}

说明

  • async_read_file 实际上没有真正的异步 I/O,文件读取是同步完成的。若要真正异步读取,需使用系统的异步 I/O API(如 Linux 的 io_uring 或 Windows 的 ReadFileEx)包装成 awaitable
  • 示例演示了协程之间的组合与 co_await 的使用,真正的异步 I/O 只需替换读取逻辑即可。

四、最佳实践

经验 说明
**尽量返回 `awaitable
** | 保持函数签名清晰,调用方可直接co_await`。
避免在协程内部创建大量临时对象 协程挂起时局部变量会被保存,若对象过大会增加堆开销。
使用 asio::co_spawn 统一协程启动与错误处理,避免手动管理句柄。
正确处理异常 promise_type::unhandled_exception 应将异常转发到协程句柄或外层。
考虑 await_ready 的实现 对于同步完成的操作,可在 await_ready 返回 true 以避免挂起。

五、常见坑与调试技巧

对策
忘记 co_return 编译器会报“expected ‘co_return’”错误。
协程对象在栈上失效 确保协程句柄存活到 io_context.run() 完成;最好通过 std::shared_ptr 持有。
异常未捕获 在协程入口使用 try/catch 包裹,或在 promise_type::unhandled_exception 中处理。
协程挂起后不恢复 检查 await_suspend 返回 true(挂起)与 false(不挂起)的逻辑。
调试输出混乱 使用 asio::detail::debug_outputspdlog,并在 io_contextio_service::strand 上同步输出。

六、总结

C++20 协程为异步编程提供了更直观、可组合的语义,降低了回调地狱的风险。配合成熟的 I/O 框架(如 asio)可以实现高性能、可维护的网络或文件处理。掌握 awaitable 接口、状态机展开机制与协程句柄管理,是成为现代 C++ 开发者的关键技能。祝你在协程的世界里编写出流畅且高效的异步程序!

发表评论