**C++20 中的协程:实现协作式多任务的原理与示例**

协程(coroutine)是 C++20 引入的语法糖,为实现协作式多任务提供了原生语言支持。它使得函数可以在执行过程中“挂起”并在未来某个时点恢复,既能保持可读性,又能避免传统线程调度带来的复杂性。下面从概念、关键类型、实现细节以及一个完整示例四个部分展开说明。


1. 协程的基本概念

  • 挂起(Suspend):协程执行到 co_await, co_yieldco_return 时,函数体暂停,状态被保存。
  • 恢复(Resume):外部或内部再次调用协程时,它从上一次挂起的位置继续执行。
  • 协作式:协程的切换是由程序显式触发,而非由调度器抢占。

协程本质上是对函数控制流的拆分:把一次完整的执行过程拆成若干段,每段之间通过协程句柄(std::coroutine_handle)进行连接。


2. 关键类型与概念

关键词 作用 说明
co_await 挂起协程,等待 awaitable 完成 需要实现 await_ready, await_suspend, await_resume
co_yield 生成值,挂起协程并返回 需要实现 await_suspend 以便生成器模式
co_return 结束协程,返回值 return 类似,但会触发清理
promise_type 协程的承诺对象,管理协程状态 必须定义在返回类型中
std::coroutine_handle 控制协程执行的句柄 通过 co_await 或外部调用 resume()

注意:协程返回类型必须提供 promise_type,并且要满足 std::suspend_always / std::suspend_never 等约束。


3. 典型协程实现流程

  1. 编译器解析:在协程体中遇到 co_await / co_yield / co_return,编译器将函数拆分为若干“悬挂点”。
  2. 生成 promise_type:每个协程都有一个 promise_type 实例,用来保存状态、返回值、异常等。
  3. 创建 coroutine_handle:返回类型(通常是包装器)持有 `std::coroutine_handle `,用于后续恢复与销毁。
  4. 挂起:在 co_await 时,调用 await_suspend,可能返回 true/false 决定是否立即恢复;在 co_yield 时,生成一个值并挂起。
  5. 恢复:外部调用 handle.resume(),函数从挂起点继续执行,直至下一个挂起或结束。

4. 示例:一个异步整数生成器

下面实现一个简单的异步整数生成器,演示协程的挂起、恢复和值传递。

#include <iostream>
#include <coroutine>
#include <optional>

struct AsyncIntGenerator {
    struct promise_type {
        std::optional <int> value;
        std::suspend_always initial_suspend() { return {}; } // 立即挂起,等外部 resume
        std::suspend_always final_suspend() noexcept { return {}; }
        AsyncIntGenerator get_return_object() {
            return AsyncIntGenerator{ std::coroutine_handle <promise_type>::from_promise(*this) };
        }
        void unhandled_exception() { std::terminate(); }
        void return_void() {}

        // co_yield 触发
        std::suspend_always yield_value(int v) {
            value = v;
            return {};  // 挂起
        }
    };

    std::coroutine_handle <promise_type> handle;

    AsyncIntGenerator(std::coroutine_handle <promise_type> h) : handle(h) {}
    ~AsyncIntGenerator() { if (handle) handle.destroy(); }

    bool resume() {
        if (!handle.done()) {
            handle.resume();
            return true;
        }
        return false;
    }

    std::optional <int> get() const { return handle.promise().value; }
};

AsyncIntGenerator async_range(int start, int count) {
    for (int i = 0; i < count; ++i)
        co_yield start + i;
}

int main() {
    auto gen = async_range(10, 5);
    while (gen.resume()) {
        std::cout << "Got: " << *gen.get() << '\n';
    }
    std::cout << "Finished\n";
}

输出

Got: 10
Got: 11
Got: 12
Got: 13
Got: 14
Finished

说明:async_range 在第一次 resume() 时执行到 co_yield 10,挂起并返回值;随后每次 resume() 继续执行,直到循环结束。


5. 常见坑点与最佳实践

场景 坏习惯 推荐做法
异常处理 co_yield 时忽略异常 promise_type::unhandled_exception 里抛出/记录
资源管理 协程返回后忘记 handle.destroy() 在包装器的析构中自动销毁,或使用 RAII
调度 频繁 resume() 导致性能瓶颈 对于高频场景,考虑使用事件循环或线程池
线程安全 直接在多线程中访问同一个 coroutine_handle 使用互斥锁或线程安全的协程框架

6. 进一步阅读

  • Bjarne Stroustrup, The C++ Programming Language (最新版),第 42 章
  • cppreference.com 协程章节
  • 《C++ Concurrency in Action》 – 对协程与传统并发的对比分析

总结
C++20 的协程提供了一种轻量级、语义清晰的方式来实现协作式多任务。通过 co_awaitco_yieldco_return,程序员可以像写顺序代码一样表达异步逻辑,真正做到“读代码即为执行”。熟练掌握协程后,你将能在高性能服务器、游戏引擎或任何需要并发 I/O 的场景中大展拳脚。

发表评论