C++20中协程的使用与实现细节

在C++20中,协程(coroutines)被正式纳入语言标准,提供了一种更高层次的异步编程模型。相比传统的回调或Future/Promise,协程让代码更像同步流程,更易读、维护。本文将从语法、实现原理、典型使用场景以及常见坑点四个角度,系统地剖析C++20协程的核心概念与实践。

1. 协程基本语法

1.1 co_awaitco_yieldco_return

  • co_await:等待一个协程或异步操作完成,类似await
  • co_yield:生成一个值给消费者,类似生成器。
  • co_return:结束协程,返回最终结果。

1.2 协程函数的返回类型

C++20将协程函数的返回类型分为三类:

  • `std::future ` / `std::shared_future`(标准库实现)
  • `std::generator `(C++23提供)
  • 自定义 `awaitable ` 或 `generator`(常见于Boost.Asio等)

编译器会自动插入一个“promise”对象,协程函数的状态机由此生成。

1.3 示例代码

#include <iostream>
#include <coroutine>
#include <thread>
#include <chrono>

struct Task {
    struct promise_type {
        Task get_return_object() { return {}; }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };
};

Task simple() {
    std::cout << "Before co_await\n";
    co_await std::suspend_always{};   // 模拟异步等待
    std::cout << "After co_await\n";
}

int main() {
    simple();
    std::cout << "Program end\n";
    return 0;
}

该程序会先输出 “Before co_await”,随后暂停,最后在 main 结束前输出 “After co_await”。

2. 协程实现原理

2.1 状态机生成

编译器把协程函数拆分为若干“步”,每个 co_await/co_yield/co_return 处都会产生一条分支。所有分支通过一个 switch 或状态机表驱动,保证程序在暂停后能够恢复到正确的位置。

2.2 Promise 对象

Promise 对象负责:

  • 保存协程局部变量(如 this、参数等)
  • 提供 get_return_objectinitial_suspendfinal_suspendreturn_void/return_valueunhandled_exception 等接口
  • 存放协程的“悬挂”或“完成”状态

2.3 资源管理

协程生成器对象与其 promise_type 必须在同一生命周期内,否则会出现悬挂的 std::coroutine_handle

  • co_return 时,final_suspend 的返回值决定协程是否自动销毁。
  • 若返回 std::suspend_never,协程会立即完成并销毁。

3. 常见协程使用场景

3.1 异步 I/O

std::async 或网络库中,协程可替代回调链,让异步 I/O 看似同步。

awaitable<std::string> fetch_from_server(const std::string& url) {
    auto socket = co_await tcp_connect(url);
    std::string resp = co_await socket.read_some();
    co_return resp;
}

3.2 生成器

使用 co_yield 可实现懒加载序列,例如斐波那契数列、文件行迭代器等。

generator <int> fibonacci() {
    int a = 0, b = 1;
    while (true) {
        co_yield a;
        int tmp = a + b;
        a = b;
        b = tmp;
    }
}

3.3 任务调度

在游戏引擎或 UI 框架中,协程可做帧间调度,让长时间任务分步执行。

4. 常见坑与最佳实践

序号 坑点 解决方案
1 协程对象被移动后访问 coroutine_handle 确保协程对象在移动前后保持有效,避免裸指针
2 co_await 的异步对象没有 await_ready/await_suspend 自定义 awaitable 时实现完整协议
3 内存泄漏:promise 未被销毁 final_suspend 必须返回 std::suspend_never 或手动销毁 handle
4 性能开销 使用 std::suspend_always / std::suspend_never 选择性暂停,避免不必要的上下文切换
5 与旧版库混用 尽量使用统一的异步框架(如 Boost.Asio),避免不同协程实现混合

4.1 自定义 awaitable 示例

struct Timer {
    std::chrono::milliseconds duration;
    Timer(std::chrono::milliseconds d) : duration(d) {}
    bool await_ready() const noexcept { return duration.count() == 0; }
    void await_suspend(std::coroutine_handle<> h) {
        std::thread([h, this](){
            std::this_thread::sleep_for(duration);
            h.resume();
        }).detach();
    }
    void await_resume() const noexcept {}
};

调用方式:

co_await Timer( std::chrono::seconds(1) );

5. 小结

C++20 的协程为语言带来了更接近自然流程的异步编程模型。通过了解协程的语法、状态机实现以及 Promise 的角色,程序员可以在保持代码可读性的同时,高效地实现异步 I/O、生成器、任务调度等功能。与此同时,需要注意协程对象的生命周期、awaitable 的完整协议以及性能权衡,避免常见陷阱。随着未来标准的进一步完善(C++23 将推出 std::generator 等),协程将在更广泛的场景中发挥作用。

发表评论