从头开始学习C++20的协程:实现一个简易异步IO框架

在现代C++20中,协程(coroutines)为我们提供了一种以声明式方式编写异步代码的强大工具。本文将带你从零开始,利用协程实现一个极其简化的异步IO框架,帮助你更直观地理解协程的工作原理、状态机实现以及与事件循环的结合方式。

一、协程基本概念

协程是一种特殊的函数,它可以在执行过程中挂起(co_awaitco_yieldco_return)并在需要时恢复。C++20把协程分解为三大核心:

  1. 挂起点(awaitable):可以被co_await挂起的对象。
  2. 协程对象:由编译器生成的状态机,内部维护协程的执行状态。
  3. 协程句柄(promise):协程内部的上下文,提供挂起/恢复接口。

二、简化的事件循环

为了演示协程的实战,我们首先实现一个非常简易的事件循环:

#include <queue>
#include <functional>
#include <iostream>

using Task = std::function<void()>;

class EventLoop {
public:
    void schedule(Task t) { queue_.push(std::move(t)); }

    void run() {
        while (!queue_.empty()) {
            auto t = std::move(queue_.front());
            queue_.pop();
            t();
        }
    }

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

这里的Task是一个包装了待执行代码块的可调用对象,EventLoop会不断地弹出队列中的任务并执行。

三、创建可 await 的异步任务

我们定义一个简化的awaitable,它将协程挂起并在稍后通过事件循环恢复:

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

struct AsyncSleep {
    std::chrono::milliseconds dur;
    EventLoop& loop;

    struct awaiter {
        std::chrono::milliseconds dur;
        EventLoop& loop;
        std::chrono::steady_clock::time_point start;

        bool await_ready() const noexcept { return false; }
        void await_suspend(std::coroutine_handle<> h) {
            // 将恢复工作包装成任务
            loop.schedule([h, dur = dur, start = start]() {
                auto now = std::chrono::steady_clock::now();
                if (now - start < dur) {
                    // 若未到时,重新调度
                    loop.schedule([h, dur, start]() { h.resume(); });
                } else {
                    h.resume();
                }
            });
            start = std::chrono::steady_clock::now();
        }
        void await_resume() noexcept {}
    };

    awaiter operator co_await() noexcept {
        return awaiter{dur, loop, std::chrono::steady_clock::now()};
    }
};

AsyncSleepco_await时会挂起协程,并在指定时间后通过事件循环恢复。

四、协程函数的实现

下面是一个使用上述AsyncSleep的协程函数示例:

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

AsyncTask my_async_job(EventLoop& loop) {
    std::cout << "Task start\n";
    co_await AsyncSleep{std::chrono::milliseconds(500), loop};
    std::cout << "Halfway through\n";
    co_await AsyncSleep{std::chrono::milliseconds(500), loop};
    std::cout << "Task finished\n";
}

这里我们用自定义的promise_type让协程始终在同步调用点执行,真正的挂起/恢复逻辑由AsyncSleep处理。

五、主程序驱动

int main() {
    EventLoop loop;
    // 启动协程
    auto task = my_async_job(loop);
    // 将协程包装成一个可执行任务并加入事件循环
    loop.schedule([&loop, task = std::move(task)]() mutable {
        // 由于我们使用的是std::suspend_never,协程已在这里完成
    });

    // 运行事件循环
    loop.run();
    return 0;
}

运行上述代码,你将看到:

Task start
Halfway through
Task finished

两次AsyncSleep之间大约延迟了1秒,证明协程挂起/恢复与事件循环协作顺利。

六、总结与展望

  • 协程是把异步代码写成同步样式的强大手段。
  • 通过awaitable事件循环的组合,可以实现高度可组合的异步框架。
  • 本示例极度简化,真正的IO协程需要结合系统层面的多路复用(epoll/kqueue)以及线程池等组件。

你可以进一步扩展:

  • 使用std::future/std::promise包装协程返回值。
  • 将事件循环改为多线程,支持并发调度。
  • 对接网络套接字,实现真正的异步服务器。

C++20协程正在快速成熟,掌握它将为你开启更高效、更可维护的异步编程之路。祝你编码愉快!

发表评论