C++20协程:简化异步编程的实践

在 C++20 中,协程(coroutines)被引入为一种语言级别的异步编程机制。相较于传统的线程或基于回调的异步模型,协程能够让开发者用同步代码的语法书写异步逻辑,从而提升可读性和维护性。本文将从协程的基础概念出发,演示如何在 C++20 中使用协程实现一个简单的异步任务调度器,并讨论常见的陷阱与最佳实践。

1. 协程的核心概念

  • co_await:挂起当前协程,等待一个可等待对象(awaitable)完成。
  • co_yield:生成一个值并挂起协程,等待调用方继续。
  • co_return:返回协程的最终结果并结束协程。
  • Awaitable:实现 await_ready()await_suspend()await_resume() 的对象,决定挂起行为、恢复方式和返回值。

协程本质上是一种“暂停点”集合。每个暂停点都是一个状态机,编译器在编译期将其拆解成生成函数,并在运行时通过 promise_type 与调用者交互。

2. 一个最小的协程实现

下面给出一个最简的 async_task,它包装了一个可以被 co_await 的协程。

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

template<typename T>
struct async_task {
    struct promise_type;
    using handle_type = std::coroutine_handle <promise_type>;

    struct promise_type {
        T value_;
        std::exception_ptr exception_;

        async_task get_return_object() {
            return async_task{handle_type::from_promise(*this)};
        }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void unhandled_exception() { exception_ = std::current_exception(); }
        template<typename U>
        void return_value(U&& val) { value_ = std::forward <U>(val); }
    };

    handle_type h_;
    explicit async_task(handle_type h) : h_(h) {}
    ~async_task() { if (h_) h_.destroy(); }

    // 让外部能够使用 awaitable 接口
    auto operator co_await() const {
        struct awaiter {
            handle_type h_;
            bool await_ready() const noexcept { return false; }
            void await_suspend(std::coroutine_handle<> caller) const noexcept {
                // 这里简化为直接在线程中执行
                std::thread([h=std::move(h_), caller](){
                    h.resume();
                    caller.resume();
                }).detach();
            }
            T await_resume() {
                if (h_.promise().exception_) std::rethrow_exception(h_.promise().exception_);
                return h_.promise().value_;
            }
        };
        return awaiter{h_};
    }
};

async_task <int> async_add(int a, int b) {
    std::cout << "Start async_add in thread " << std::this_thread::get_id() << "\n";
    std::this_thread::sleep_for(std::chrono::seconds(1));
    co_return a + b;
}

int main() {
    auto task = async_add(3, 4);
    int result = task.await_resume(); // 这里直接拿值,实际使用中会使用 co_await
    std::cout << "Result: " << result << "\n";
}

注意:上述代码仅作演示,实际项目中需要更完善的异常处理与线程池管理。

3. 构建一个简单的任务调度器

在实际使用中,往往需要把多个协程调度到一个线程池中,以减少线程上下文切换。下面给出一个极简的调度器实现:

#include <coroutine>
#include <vector>
#include <queue>
#include <thread>
#include <condition_variable>
#include <atomic>

class TaskScheduler {
public:
    using Task = std::coroutine_handle<std::suspend_always::promise_type>;

    TaskScheduler(size_t thread_count = std::thread::hardware_concurrency())
        : stop_(false)
    {
        for (size_t i = 0; i < thread_count; ++i)
            workers_.emplace_back([this](){ this->worker_loop(); });
    }

    ~TaskScheduler() {
        stop_ = true;
        cv_.notify_all();
        for (auto& th : workers_) th.join();
    }

    void enqueue(Task t) {
        {
            std::lock_guard<std::mutex> lk(mtx_);
            queue_.push(t);
        }
        cv_.notify_one();
    }

private:
    void worker_loop() {
        while (!stop_) {
            Task t;
            {
                std::unique_lock<std::mutex> lk(mtx_);
                cv_.wait(lk, [&]{ return stop_ || !queue_.empty(); });
                if (stop_ && queue_.empty()) return;
                t = queue_.front();
                queue_.pop();
            }
            t.resume();
        }
    }

    std::vector<std::thread> workers_;
    std::queue <Task> queue_;
    std::mutex mtx_;
    std::condition_variable cv_;
    std::atomic <bool> stop_;
};

结合 async_task,你可以将协程包装为 TaskScheduler::Task 并提交:

TaskScheduler scheduler;

auto task = async_add(10, 20);
// 将协程包装为 awaitable,然后提交
scheduler.enqueue(task.get_coro_handle());

4. 常见陷阱与最佳实践

  1. 不要在协程内部直接调用 std::this_thread::sleep_for
    这会阻塞当前线程,导致协程失去异步的意义。推荐使用异步定时器(如 Boost.Asio 的 steady_timer)来挂起协程。

  2. 避免在 await_suspend 中捕获异常
    如果在挂起期间抛出异常,协程的 promise_type 会记录该异常。务必在 await_resume 处正确恢复。

  3. 避免返回局部对象
    协程返回的对象会在协程结束后被销毁,若使用引用或指针要确保生命周期安全。

  4. 使用 co_await std::suspend_alwayssuspend_never
    根据需求决定是否在协程起始/结束处挂起,避免不必要的上下文切换。

  5. 线程安全的协程返回值
    若协程结果需要在多线程间共享,建议使用 std::promise/std::future 之类的同步原语包装,或在协程内部自行同步。

5. 结语

C++20 的协程为异步编程提供了一种更直观、类型安全的手段。虽然在实现上仍需要对 promise_type、awaitable 以及调度机制有一定的理解,但通过正确的设计,协程可以大幅简化异步代码结构,提升可读性。希望本文的示例能帮助你快速上手 C++20 协程,并在实际项目中得到应用。

发表评论