C++20 协程:从底层实现到实战应用

C++20 为协程提供了官方支持,使得异步编程和生成器等模式得以在语言层面优雅实现。本文将先从协程的底层原理说起,然后给出一个完整的协程生成器示例,并讨论常见的使用场景与注意事项。

1. 协程的基本概念

协程是可挂起和恢复的函数。与传统线程不同,协程的切换由程序控制,成本更低,适合大量并发的场景。C++20 通过三大关键类型实现协程:

  1. std::coroutine_handle – 负责协程的生命周期管理。
  2. std::suspend_always / std::suspend_never – 决定协程何时挂起或不挂起。
  3. promise_type – 用户定义的协程承诺,用于传递返回值、异常以及挂起/恢复逻辑。

协程的执行流程:

  • 调用协程函数时,编译器生成一个状态机。
  • 初始挂起(如果 initial_suspend() 返回 suspend_always)。
  • 通过 co_awaitco_yieldco_return 控制挂起/恢复。
  • 当协程结束时,编译器会自动清理资源并返回 promise_type

2. 一个简单的生成器协程

下面实现一个整数序列生成器,演示如何使用 co_yieldpromise_type

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

template<typename T>
struct generator {
    struct promise_type {
        std::optional <T> current_value;

        // 当协程第一次被调用时执行
        generator get_return_object() {
            return generator{
                std::coroutine_handle <promise_type>::from_promise(*this)
            };
        }

        std::suspend_always initial_suspend() noexcept { return {}; }
        std::suspend_always final_suspend() noexcept   { return {}; }

        void unhandled_exception() { std::exit(1); }

        // 通过 co_yield 设置返回值
        std::suspend_always yield_value(T value) noexcept {
            current_value = std::move(value);
            return {};
        }

        void return_void() {}
    };

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

    bool move_next() {
        coro.resume();
        return !coro.done();
    }

    T current() const { return *coro.promise().current_value; }
};

generator <int> range(int start, int end) {
    for (int i = start; i <= end; ++i)
        co_yield i;
}

使用方式:

int main() {
    auto seq = range(1, 5);
    while (seq.move_next())
        std::cout << seq.current() << ' ';
    // 输出:1 2 3 4 5
}

该示例展示了:

  • co_yield 用来产生值。
  • promise_type 保存当前值。
  • move_next 调用 resume,让协程继续执行到下一个挂起点。

3. 异步协程示例

C++20 的协程还可以与 std::futurestd::async 结合,形成 co_await 的异步调用。

#include <future>
#include <iostream>
#include <thread>

std::future <int> async_add(int a, int b) {
    return std::async(std::launch::async, [a, b]() {
        std::this_thread::sleep_for(std::chrono::seconds(1));
        return a + b;
    });
}

std::future <int> compute() {
    int x = co_await async_add(5, 10); // 异步等待
    int y = co_await async_add(x, 20);
    co_return y;
}

此处 co_await 会在 async_add 返回的 std::future 完成时恢复协程,从而实现简洁的异步链。

4. 实战场景与注意事项

场景 协程优势 注意点
生成器 轻量级、无额外线程 必须手动管理生命周期,避免悬空句柄
异步 IO 代码可读性高 需要与事件循环或线程池配合
协作式调度 能精准控制任务切换 过度使用会导致堆栈碎片化
网络编程 适合高并发 需要把协程与网络库(如 Boost.Asio)结合
  • 异常安全:若协程内部抛出异常,promise_type::unhandled_exception 会被调用,需要自行捕获并处理。
  • 资源泄露:协程句柄不自动销毁,必须在 generator 等包装类中调用 destroy()
  • 编译器支持:目前 GCC 10+、Clang 11+、MSVC 19.28+ 已支持完整协程。建议使用较新的标准库实现。

5. 结语

C++20 的协程让异步编程和生成器等高级模式在语言层面得到统一支持,既保持了 C++ 的性能优势,又显著提升了代码可读性和维护性。虽然协程本身是一个强大的工具,但在实际项目中仍需结合业务需求、性能预算与团队经验做出权衡。希望本文能帮助你快速入门协程,并在自己的项目中灵活运用。

发表评论