**C++20 的三方协程:构建可组合、可伸缩的异步任务**

在 C++20 里,协程成为标准库的一部分,极大地简化了异步编程的写法。与传统回调、事件循环或多线程相比,协程可以用同步式代码写出异步逻辑,并保持代码的可读性。本文将介绍三方协程的基本结构、使用 std::generatorstd::task 的技巧,以及如何将协程与线程池、事件循环结合,构建高性能可伸缩的应用。

1. 协程的核心概念

协程是可以挂起(co_await)和恢复(co_yieldco_return)的函数。它们生成一个可被外部控制的对象,内部维护执行状态。C++20 为协程提供了两种重要的返回类型:

  • `std::generator `:返回一个可迭代的生成器,适合顺序产生一系列值。
  • `std::task `:返回一个代表异步操作的任务,能够与 `co_await` 一起使用。

2. 实现一个自定义 std::task 类型

标准库中没有直接提供 std::task,但我们可以用 std::futurestd::shared_future 或自定义 Promise/Handle 组合实现。下面给出一个简化的实现示例:

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

    struct promise_type {
        T value_;
        std::exception_ptr exc_;
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }

        auto get_return_object() { return Task{handle_type::from_promise(*this)}; }
        void unhandled_exception() { exc_ = std::current_exception(); }
        template<typename U>
        void return_value(U&& v) { value_ = std::forward <U>(v); }
    };

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

    T get() {
        if (h_) {
            h_.resume();
            if (h_.done()) {
                if (h_.promise().exc_) std::rethrow_exception(h_.promise().exc_);
                return std::move(h_.promise().value_);
            }
        }
        throw std::runtime_error("task not ready");
    }

private:
    handle_type h_;
};

此实现允许我们写:

Task <int> asyncAdd(int a, int b) {
    co_return a + b;
}

3. 与线程池的协作

协程本身不占用线程,它们的挂起点会将控制权交还给调用者。要在多线程环境中真正并行执行,需要将协程调度到线程池。一个常见方案是:

  1. 创建 std::thread 数量的工作线程,维护一个线程安全的任务队列。
  2. co_await 的操作会把协程句柄放入队列,线程池线程从队列取出并 resume
  3. 当协程执行完毕后,线程池将其销毁或归还给回收池。
class ThreadPool {
    std::vector<std::thread> workers_;
    std::queue<Task<void>::handle_type> queue_;
    std::mutex mtx_;
    std::condition_variable cv_;
    bool stop_ = false;

public:
    ThreadPool(size_t n) {
        for (size_t i=0; i<n; ++i)
            workers_.emplace_back([this] { workerLoop(); });
    }

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

    void workerLoop() {
        while (true) {
            Task <void>::handle_type h;
            {
                std::unique_lock<std::mutex> lk(mtx_);
                cv_.wait(lk, [this] { return stop_ || !queue_.empty(); });
                if (stop_ && queue_.empty()) return;
                h = queue_.front(); queue_.pop();
            }
            h.resume(); // 继续协程执行
        }
    }

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

4. 示例:并发网络请求

假设我们有一个异步 HTTP 客户端 fetchAsync(url) 返回 Task<std::string>,我们想并发获取 10 个 URL,并在全部完成后处理结果。代码示例:

ThreadPool pool(4); // 4 条工作线程

Task<std::vector<std::string>> fetchAll(const std::vector<std::string>& urls) {
    std::vector<std::string> results;
    for (const auto& u : urls) {
        // 每个请求放到线程池执行
        pool.submit(fetchAsync(u).get_return_object().handle_);
        results.push_back(co_await fetchAsync(u)); // 简化示例
    }
    co_return results;
}

在实际代码中,fetchAsync 会在内部使用 co_await 等待网络 I/O,并把协程句柄返回给线程池,让工作线程继续执行。

5. 调试与性能提示

  • 协程切换成本:每次 co_await 都会产生一次上下文切换,尽量把耗时的操作放在 I/O 之外,例如使用 std::async 或线程池。
  • 异常传播:协程内部抛出的异常会存入 promise,外部通过 Task::get() 捕获。记得在协程函数中使用 try/catch 处理已知异常。
  • 可伸缩性:协程本身非常轻量,关键是调度器。使用多核时,线程池的工作线程数可与 CPU 核数相匹配,或根据 I/O 密集度动态调整。

6. 结语

C++20 的协程为异步编程带来了革命性的简化。通过自定义 Task、配合线程池或事件循环,我们可以构建既易读又高效的并发程序。随着标准库的进一步完善,例如 std::async 的协程适配、std::ranges 与协程的深度结合,未来 C++ 的异步生态将更加成熟。希望本文能为你在项目中使用协程提供实用参考。

发表评论