C++20 协程:实现异步任务调度

在 C++20 标准中,协程(coroutines)被正式加入语言层面,为编写异步、非阻塞代码提供了强大且简洁的工具。本文将以一个实用示例——基于协程的异步任务调度器,展示如何利用 C++20 标准库中的 std::suspend_alwaysstd::suspend_never 以及 std::experimental::coroutine_handle 等关键组件,构建一个轻量级的任务队列,支持异步执行和结果回调。

1. 协程基础

协程是一种能够在执行过程中被挂起和恢复的函数。相比传统的线程,协程开销极小,适合高并发场景。C++20 通过 co_awaitco_yieldco_return 关键字以及协程返回类型(如 std::futurestd::generator)实现协程机制。

核心类型:

  • `std::coroutine_handle `:代表协程句柄,允许对协程进行挂起、恢复和销毁。
  • std::suspend_always:协程始终挂起,常用于实现自定义 awaitable。
  • std::suspend_never:协程从不挂起,常用于同步实现。

2. 设计思路

我们将构建一个简单的 Task 类,代表一次异步计算;Scheduler 类管理所有待执行的 Task,并在事件循环中依次恢复它们。关键点:

  1. Task 的返回类型为 `std::future `,内部使用 `co_await` 等待异步事件。
  2. Scheduler 使用 std::queue<std::coroutine_handle<>> 存储待恢复的协程句柄。
  3. 通过 co_await 自定义 awaitable,协程在等待事件时挂起,Scheduler 负责触发恢复。

3. 代码实现

#include <coroutine>
#include <future>
#include <queue>
#include <iostream>
#include <chrono>
#include <thread>
#include <optional>

struct Scheduler {
    static Scheduler& instance() {
        static Scheduler s;
        return s;
    }

    void schedule(std::coroutine_handle<> h) {
        queue_.push(h);
    }

    void run() {
        while (!queue_.empty()) {
            auto h = queue_.front();
            queue_.pop();
            h.resume();
        }
    }

private:
    std::queue<std::coroutine_handle<>> queue_;
};

struct SleepAwaitable {
    std::chrono::milliseconds duration;
    Scheduler& scheduler = Scheduler::instance();

    bool await_ready() const noexcept { return false; }
    void await_suspend(std::coroutine_handle<> h) {
        std::thread([=]{
            std::this_thread::sleep_for(duration);
            scheduler.schedule(h);
        }).detach();
    }
    void await_resume() noexcept {}
};

using Task = std::future <void>;

Task async_sleep(std::chrono::milliseconds ms) {
    co_await SleepAwaitable{ms};
    co_return;
}

Task async_task(int id, int work_units) {
    std::cout << "Task " << id << " start\n";
    for (int i = 0; i < work_units; ++i) {
        co_await SleepAwaitable{std::chrono::milliseconds(100)};
        std::cout << "Task " << id << " progress " << i+1 << "/" << work_units << "\n";
    }
    std::cout << "Task " << id << " finish\n";
    co_return;
}

int main() {
    Scheduler::instance().schedule(async_task(1, 5).operator co_await().handle_);
    Scheduler::instance().schedule(async_task(2, 3).operator co_await().handle_);
    Scheduler::instance().schedule(async_sleep(std::chrono::milliseconds(200)).operator co_await().handle_);

    Scheduler::instance().run(); // 进入事件循环
    std::this_thread::sleep_for(std::chrono::seconds(3)); // 让后台线程有足够时间完成
    return 0;
}

说明

  • SleepAwaitable:自定义 awaitable,内部使用 std::thread 模拟异步等待。实际项目中可替换为 I/O 完成事件或定时器回调。
  • Scheduler::schedule:把挂起的协程句柄放入队列。
  • Scheduler::run:循环恢复协程,直至队列为空。因为协程在挂起时会重新排入队列,事件循环会不断推进。

4. 优点与扩展

  • 轻量级:协程本质上是状态机,内存占用极低。
  • 可组合:不同 Task 可互相 co_await,形成复杂工作流。
  • 易于调试:协程的挂起点和恢复点可以通过 IDE 或日志清晰追踪。

扩展思路:

  1. 任务优先级:改用优先级队列。
  2. 事件源:把 SleepAwaitable 替换为网络 I/O 或文件 I/O 的异步事件。
  3. 异常传播:在 Task 中捕获并转为 std::future 的异常状态。

5. 结语

C++20 的协程为异步编程带来了革命性的简化。通过结合事件循环与自定义 awaitable,开发者可以轻松实现高性能、可维护的异步任务调度器。未来随着标准库完善,协程将成为 C++ 异步生态的基石,值得每个 C++ 开发者深入掌握。

发表评论