c++20的协程(Coroutine)如何简化异步编程? (对比传统回调)

协程不能自动简化异步编程,它仅提供挂起/恢复机制,不内置调度、线程管理或运行时支持,需手动处理生命周期、避免栈变量跨挂起、显式捕获异常并依赖第三方库实现awaitable语义。

协程不能自动简化异步编程,它只是把回调地狱换成了「看似同步、实则挂起」的控制流——前提是正确管理生命周期、避免栈溢出、不滥用 co_await 在非可挂起点。

为什么 co_await 不等于「自动异步转同步」

协程本身不执行调度,也不绑定线程或事件循环。你写 co_await async_read(socket, buf),背后仍需一个实现了 awaitable 接口的对象(比如返回 task 的函数),而该对象的 await_suspend 才真正注册回调到 I/O 多路复用器(如 epoll 或 IOCP)。

常见误判是以为只要用了 co_await,就能像 Python 的 async/await 那样开箱即用。C++20 没有标准运行时,所有调度逻辑必须自己搭或依赖第三方库(如 libunifex、cppcoro、Boost.ASIO 1.78+)。

  • co_await 只触发挂起/恢复协议,不启动任何线程、不管理线程池
  • 没有 asyncio.run() 对应物;你得手动调用 task.start()executor.run()
  • 忘记 co_return 或在销毁前未完成协程,会导致未定义行为(UB),而非抛异常

对比传统回调:代码结构变了,但错误点更隐蔽

传统回调的问题是嵌套深、错误传播难、状态分散;协程表面扁平,但引入了新的崩溃路径:

task handle_request(tcp_socket& sock) {
    auto buf = std::make_unique(1024);
    // ✅ 正确:buf 生命周期覆盖整个协程
    ssize_t n = co_await sock.async_read(buf.get(), 1024);
    co_await sock.async_write("HTTP/1.1 200 OK\r\n", 18);
}

下面这段就危险:

task bad_example(tcp_socket& sock) {
    char local_buf[1024]; // ❌ 栈变量,协程挂起后可能已被销毁
    co_await sock.async_read(local_buf, 1024); // UB 高发区
}
  • 协程栈帧在挂起时被移到堆上(由 promise_type::get_return_object_on_allocation 决定),但局部变量仍是栈分配 —— 必须确保其生命周期跨挂起点
  • 异常仍需手动捕获:try { co_await op(); } catch(...) { ... },不会自动传播到调用方协程
  • 调试困难:gdb 对 co_await 行的单步支持有限,常跳过挂起点直接到恢复点

哪些场景下协程确实比回调更可靠?

当异步操作有明确顺序依赖、且中间状态需多次复用时,协程优势明显。例如实现一个带重试、超时、进度通知的文件下载:

task download_with_retry(http_client& client, string_view url) {
    for (int i = 0; i < 3; ++i) {
        auto res = co_await client.get(url); // 等待完整响应
        if (res.status == 200) {
            co_await write_to_disk(res.body);
            co_return true;
        }
        co_await timer::sleep(1s); // 挂起而不阻塞线程
    }
    co_return false;
}
  • 相比回调嵌套(on_success → on_write_complete → on_timeout),状态变量(重试次数、临时 buffer)自然保留在作用域内
  • 超时可统一用 co_await with_timeout(op, 5s) 封装,无需为每个回调单独设 timer ID 并清理
  • 但注意:timer::sleep 必须是真正的 awaitable(内部调用 epoll_waitWaitForSingleObject),不是 std::this_thread::sleep_for

协程最大的陷阱不是语法,而是误以为它解决了资源生命周期问题——它恰恰让生命周期更难追踪。栈变量、裸指针、未 move 的 unique_ptr,在挂起点前后都可能失效。写协程代码时,眼睛要盯着「这个变量在下次 co_await 返回时还活着吗?」,而不是只看缩进是否整齐。