2026-01-09 10:32:47: C++协程的最佳实践

在 C++20 中,协程(coroutines)为异步编程提供了极大的便利。与传统的回调或线程模型相比,协程使代码更易读、维护性更好,且性能更优。下面通过一个最小化示例,演示如何在 C++20 中创建、使用以及优化协程,并提供一些常见的陷阱与最佳实践。


1. 基础概念回顾

  • awaiter:实现 await_ready, await_suspend, await_resume 的对象。
  • generator:最常见的协程形态,用于按需生成值。
  • task:封装异步操作,通常返回 std::future 或自定义状态。

2. 一个简单的生成器

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

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

        auto get_return_object() { return generator{std::coroutine_handle <promise_type>::from_promise(*this)}; }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void unhandled_exception() { std::exit(1); }

        template <typename U>
        std::suspend_always yield_value(U&& v) {
            current_value = std::forward <U>(v);
            value_ = current_value;
            return {};
        }

        void return_void() {}
    };

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

    bool next() {
        if (!handle_.done()) {
            handle_.resume();
            return !handle_.done();
        }
        return false;
    }

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

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

使用示例:

int main() {
    auto gen = range(0, 5);
    while (gen.next())
        std::cout << gen.current() << " ";
}

输出:0 1 2 3 4

3. 协程与 std::future

虽然协程可以直接返回值,但在现代 C++ 中,最常见的做法是将协程包装成 std::future,实现异步任务。下面给出一个 async_task 的简易实现:

#include <future>
#include <coroutine>

template <typename T>
struct async_task {
    struct promise_type {
        T result_;
        std::exception_ptr eptr_;

        auto get_return_object() {
            return async_task{std::coroutine_handle <promise_type>::from_promise(*this)};
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }

        void unhandled_exception() { eptr_ = std::current_exception(); }
        void return_value(T value) { result_ = std::move(value); }

        std::future <T> get_future() {
            return std::future <T>(handle_);
        }
    };

    std::coroutine_handle <promise_type> handle_;
    explicit async_task(std::coroutine_handle <promise_type> h) : handle_(h) {}
    ~async_task() { if (handle_) handle_.destroy(); }

    std::future <T> get_future() { return handle_.promise().get_future(); }
};

使用:

async_task <int> async_add(int a, int b) {
    co_return a + b;
}

int main() {
    auto task = async_add(3, 4);
    std::future <int> fut = task.get_future();
    std::cout << "Result: " << fut.get() << '\n';
}

4. 性能与资源管理

4.1 关闭不必要的挂起

协程的默认 suspend_always 会在每次 co_yieldco_return 时挂起。若不需要暂停,可使用 suspend_never,从而避免一次无意义的上下文切换:

std::suspend_never initial_suspend() { return {}; }  // 立即开始执行

4.2 std::shared_futuretask

如果多个消费者需要同一个协程结果,使用 std::shared_future 可避免重复执行:

auto fut = std::shared_future <int>(task.get_future());

4.3 错误传播

协程内部抛出的异常会被 unhandled_exception 捕获并保存为 exception_ptr。用户可以在 future::get() 时重新抛出,保持错误信息完整。

5. 常见陷阱

  1. 忘记销毁协程句柄:协程句柄必须在不再使用时 destroy(),否则会导致内存泄漏。
  2. 过度使用 co_yield:如果每次 co_yield 都需要一次上下文切换,性能会下降。可考虑将值一次性生成或使用批处理。
  3. 不兼容的返回类型co_return 必须匹配 promise_type::return_value 的签名,否则编译失败。

6. 进一步阅读

  • 《C++ Coroutines》作者: Herb Sutter, Niall Douglas
  • cppreference.com: std::generator, std::future, std::coroutine_handle

小结

C++20 协程通过让编译器处理挂起与恢复细节,极大简化了异步代码的写法。掌握协程的基础结构(promise、awaiter、handle),并结合 std::future 与资源管理模式,能够让你在需要高并发、低延迟的场景下写出既高效又易读的代码。祝你在 C++ 的协程旅程中愉快前行!

发表评论