在现代C++20中,协程(coroutines)为我们提供了一种以声明式方式编写异步代码的强大工具。本文将带你从零开始,利用协程实现一个极其简化的异步IO框架,帮助你更直观地理解协程的工作原理、状态机实现以及与事件循环的结合方式。
一、协程基本概念
协程是一种特殊的函数,它可以在执行过程中挂起(co_await、co_yield、co_return)并在需要时恢复。C++20把协程分解为三大核心:
- 挂起点(awaitable):可以被
co_await挂起的对象。 - 协程对象:由编译器生成的状态机,内部维护协程的执行状态。
- 协程句柄(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()};
}
};
该AsyncSleep在co_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协程正在快速成熟,掌握它将为你开启更高效、更可维护的异步编程之路。祝你编码愉快!