**如何在 C++20 中实现高效的协程式并行**

协程是 C++20 的重要特性之一,它让我们可以以同步的方式编写异步代码,并显著提升并行任务的可读性与性能。本文将从协程的基本概念、实现细节到实际使用示例,系统阐述如何在 C++20 中构建高效的协程式并行模型。


1. 协程的核心概念

  • Promise / Promise Object:协程函数返回一个 std::coroutine_handle<>,而协程内部维护一个 Promise 对象,它负责协程的生命周期与状态。
  • Awaitable:任何可以被 co_await 的对象都称为 Awaitable。它需要实现 await_ready(), await_suspend(), await_resume() 三个成员函数。
  • Suspension Pointsco_await, co_yield, co_return 触发协程挂起或返回。

协程并不是线程,而是协作式调度的轻量级任务,真正的并行实现需要与线程池或调度器配合。


2. 搭建协程调度器

下面给出一个最小化的协程调度器实现示例,支持任务的提交、挂起与恢复。

#include <coroutine>
#include <queue>
#include <functional>
#include <memory>
#include <iostream>

struct Task {
    struct promise_type {
        Task get_return_object() { return {}; }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() { std::terminate(); }
    };
};

class Scheduler {
public:
    void schedule(std::function<void()> fn) {
        tasks.push(std::move(fn));
    }

    void run() {
        while (!tasks.empty()) {
            auto fn = std::move(tasks.front());
            tasks.pop();
            fn(); // 执行协程体
        }
    }
private:
    std::queue<std::function<void()>> tasks;
};
  • Task 是协程的返回类型。
  • Scheduler 用队列管理待执行的协程函数。

在实际项目中,可将 Schedulerstd::thread 结合,实现真正的并发执行。


3. Awaitable 示例:异步 I/O

假设我们使用一个简易的异步 I/O 模拟器 AsyncRead

#include <coroutine>
#include <chrono>
#include <thread>

struct AsyncRead {
    struct promise_type { /* 省略实现 */ };
    using handle_type = std::coroutine_handle <promise_type>;

    AsyncRead(handle_type h) : coro(h) {}
    ~AsyncRead() { if (coro) coro.destroy(); }

    bool await_ready() const noexcept { return false; }
    void await_suspend(std::coroutine_handle<> awaiting) {
        // 模拟异步延迟
        std::thread([this, awaiting]() {
            std::this_thread::sleep_for(std::chrono::milliseconds(100));
            awaiting.resume();
        }).detach();
    }
    int await_resume() const noexcept { return 42; } // 假设读取到的数据

private:
    handle_type coro;
};

在协程里使用:

Task async_main() {
    int data = co_await AsyncRead{AsyncRead::promise_type{}};
    std::cout << "Read data: " << data << '\n';
}

该示例展示了如何将异步 I/O 与协程结合,await_suspend 用来挂起协程,并在后台线程完成后恢复。


4. 并行执行多个协程

下面给出一个使用 Scheduler 并行执行多个协程的完整例子。

int main() {
    Scheduler scheduler;

    auto worker = [&](int id) -> Task {
        for (int i = 0; i < 5; ++i) {
            std::cout << "Worker " << id << " step " << i << '\n';
            co_await std::suspend_always{}; // 模拟切换
        }
    };

    for (int i = 0; i < 3; ++i) {
        scheduler.schedule([&, i]() { worker(i); });
    }

    scheduler.run();
}
  • std::suspend_always{} 让协程每次循环都挂起,允许调度器交替执行其他任务。
  • 输出会呈现多任务交错执行的效果,说明协程能有效模拟并发。

5. 性能优化建议

  1. 避免过度挂起:每次挂起/恢复都涉及栈切换,过多的挂起会导致性能下降。
  2. 使用 std::suspend_never:在不需要挂起的路径上返回 suspend_never,减少调度开销。
  3. 线程池 + 协程:将协程与线程池结合,使用线程池执行真正的 CPU 密集任务,协程仅负责协作调度。
  4. 预分配栈:C++20 默认协程栈在堆上分配,若任务数量巨大,可使用自定义栈分配器降低分配次数。

6. 结语

C++20 的协程为构建高效、可读的并行程序提供了强大的工具。通过上述基础示例与调度器的搭建,你可以快速上手协程式并行开发。接下来可以进一步探索如 std::generatorstd::ranges 与协程的结合,或将协程嵌入现有的网络框架(如 Boost.Asio)中,构建真正的异步 I/O 高性能应用。祝你在 C++ 协程的世界里玩得愉快!


发表评论