探索C++中的协程:实现一个简单的异步任务调度器

协程是C++20引入的语言特性,它为编写异步代码提供了更直观、更高效的方式。相比传统的回调或状态机,协程可以让代码保持同步的写法,却在运行时实现非阻塞等待。本文将通过实现一个最小化的异步任务调度器来演示协程的基本用法,并解释关键概念与实现细节。

1. 协程基础回顾

1.1 协程的核心概念

  • 协程函数:使用co_awaitco_yieldco_return的函数被视为协程。它们的返回类型必须是std::futurestd::generator或自定义的awaitable等协程特化类型。
  • 协程句柄 (std::coroutine_handle): 用来控制协程的生命周期(resume、destroy等)。
  • Suspension(挂起): co_awaitco_yield会导致协程挂起,暂停执行直到外部恢复。

1.2 典型用例

  • I/O 事件驱动:异步读取文件、网络等
  • 任务并行:在单线程内分配多任务执行
  • 状态机简化:用协程替代手写状态机逻辑

2. 设计目标

  • 易用:只需要调用 async_task() 就能获得一个可等待的对象。
  • 低开销:使用内置的 std::coroutine_handle,避免额外的线程池或调度线程。
  • 可扩展:支持简单的任务链、错误传播和取消机制。

3. 核心组件实现

3.1 Awaitable 结构

template<typename T = void>
class Awaitable {
public:
    struct promise_type {
        std::coroutine_handle <promise_type> coro_handle;
        T value;
        std::exception_ptr eptr;

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

        void return_value(T v) { value = std::move(v); }
        void unhandled_exception() { eptr = std::current_exception(); }
    };

    using handle_type = std::coroutine_handle <promise_type>;

    explicit Awaitable(handle_type h) : handle(h) {}
    ~Awaitable() { if (handle) handle.destroy(); }

    // 让外部代码可以等待协程
    T await_resume() {
        if (handle.promise().eptr) std::rethrow_exception(handle.promise().eptr);
        return std::move(handle.promise().value);
    }

    bool await_ready() const noexcept { return false; }
    void await_suspend(std::coroutine_handle<> awaiting) {
        // 这里简单地把协程挂起,稍后在外部 resume
        awaiting.resume();
    }

    handle_type handle;
};

3.2 简易调度器

调度器维护一个任务队列,每个任务是 Awaitable 对象。通过 std::dequestd::queue 来存储,使用 std::function<void()> 作为任务包装。

class SimpleScheduler {
public:
    void post(auto&& awaitable) {
        tasks.emplace_back([awaitable = std::move(awaitable)]() mutable {
            awaitable.handle.resume();
        });
    }

    void run() {
        while (!tasks.empty()) {
            auto task = std::move(tasks.front());
            tasks.pop_front();
            task();
        }
    }

private:
    std::deque<std::function<void()>> tasks;
};

3.3 async_task 辅助函数

template<typename Func>
auto async_task(Func&& f) {
    struct Awaiter {
        std::coroutine_handle<> caller;
        std::future<decltype(f())> fut;
    };

    return Awaitable<decltype(f())>{ 
        [](Func f) -> Awaitable<decltype(f())> {
            co_await std::async(std::launch::async, f);
            co_return f();
        }(std::forward <Func>(f))
    };
}

上述示例演示了将普通函数包裹成协程的方式;实际可根据需求定制不同的 Awaitable 版本。

4. 使用示例

SimpleScheduler scheduler;

awaitable <int> compute_task() {
    std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟耗时计算
    co_return 42;
}

awaitable <void> main_task() {
    int result = co_await compute_task();
    std::cout << "Result: " << result << std::endl;
}

int main() {
    scheduler.post(main_task());
    scheduler.run();
    return 0;
}

运行后会在 2 秒后打印 Result: 42。整个流程没有使用多线程,协程在单线程内按需挂起与恢复。

5. 进阶改进

  1. 错误传播:在 promise_type::unhandled_exception() 中捕获异常,允许外部 await_resume() 通过 std::rethrow_exception 处理。
  2. 取消机制:在 promise_type 中添加 `std::atomic cancelled`,在 `await_suspend` 检查是否已取消。
  3. 多任务并行:将调度器改为事件循环模式,利用 std::condition_variable 让任务按需被唤醒。

6. 小结

协程为 C++ 提供了一种更自然的异步编程模型。通过本示例,我们构建了一个最小化的异步任务调度器,演示了协程的挂起、恢复与结果返回。虽然示例代码简洁,但它涵盖了协程使用的核心要点,为后续更复杂的异步框架(如 libuv、Boost.Asio 等)奠定了理解基础。希望能帮助你在 C++ 项目中顺利采用协程技术。

发表评论