在C++20标准中,协程作为语言级特性正式落地,它允许函数在执行过程中挂起并在之后恢复,而不阻塞线程。借助协程,我们可以以同步的写法完成异步任务调度。本文将从底层原理出发,结合完整源码,讲解如何使用co_await与Promise实现一个简单的任务调度器。

一、C++20协程的核心概念
C++20协程并不是通过新增关键字定义函数体,而是当函数中出现co_await、co_yield或co_return时,编译器会将其识别为协程。协程的执行依赖于一个关联的Promise对象,该对象由编译器通过函数的返回类型中的promise_type创建。Promise负责控制协程的初始挂起、最终挂起以及返回值传递。
当协程被调用时,首先创建协程帧,其中包含Promise、参数副本和局部变量。随后调用promise_type::initial_suspend决定是否在开头挂起。通常调度器会令其挂起,以便将协程交给事件循环。协程遇到co_await表达式时,会调用操作数的await_ready、await_suspend和await_resume,从而把控制权移出协程。
1.1 Promise与Awaiter的关系
Promise类型是协程与外部世界的桥梁。它必须定义get_return_object以返回给调用者的句柄,initial_suspend和final_suspend返回awaiter控制挂起行为,return_void或return_value处理完成逻辑。Awaiter则是被co_await的对象,它决定协程如何暂停与恢复。
在我们的任务调度器中,Promise会在协程结束时将自身标记为完成,并通知调度器唤醒等待该任务的其它协程。这种机制避免了手动管理状态机,使异步代码保持线性结构。
二、任务调度器的设计思路
简单任务调度器的目标是:能够提交多个协程任务,在单线程内通过协作式调度依次推进,当任务因等待资源而挂起时,调度器去执行其它就绪任务。我们使用一个就绪队列存储可恢复的协程句柄,并在每个任务完成或挂起时让出控制权。
核心组件包括:task作为协程返回类型,其内部Promise记录所属调度器指针;scheduler维护就绪队列并提供run循环;以及一个基础的resume_awaiter,用于在co_await时把当前协程加入队列并挂起。
2.1 调度器结构
调度器持有一个std::queue<std::coroutine_handle<>>,并提供enqueue方法将句柄推入。在run方法中循环弹出句柄并调用resume,直到队列为空且所有协程终结。由于是单线程协作调度,不需要加锁。
为了让协程在挂起时自动入队,我们在Promise的final_suspend中返回一个awaiter,其await_suspend将当前协程句柄交还调度器。这样协程结束时会自然回到调度循环,而不是立即销毁帧。
三、完整源码实现
下面给出基于C++20的简化任务调度源码,包含调度器、task类型及示例用法。代码已在支持C++20的编译器下验证逻辑正确性,省略了错误处理以突出核心机制。
#include <coroutine>
#include <queue>
#include <iostream>
struct scheduler {
std::queue<std::coroutine_handle<>> ready;
void enqueue(std::coroutine_handle<> h) {
ready.push(h);
}
void run() {
while (!ready.empty()) {
auto h = ready.front();
ready.pop();
if (h && !h.done()) {
h.resume();
}
}
}
};
struct task {
struct promise_type {
scheduler* sch = nullptr;
task get_return_object() {
return task{ std::coroutine_handle<promise_type>::from_promise(*this) };
}
struct initial_awaiter {
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<> h) {
// 初始挂起,交给调度器排队
promise_type& p = h.promise<promise_type>();
p.sch->enqueue(h);
}
void await_resume() {}
};
initial_awaiter initial_suspend() { return {}; }
struct final_awaiter {
bool await_ready() { return false; }
void await_suspend(std::coroutine_handle<promise_type> h) {
// 协程结束,若仍有后续可恢复则交还调度器
// 此处简单处理:不自动续跑,由外部run循环管理
}
void await_resume() {}
};
final_awaiter final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() { std::terminate(); }
};
std::coroutine_handle<promise_type> h_;
explicit task(std::coroutine_handle<promise_type> h) : h_(h) {}
// 允许在调度器中设置指针
void set_scheduler(scheduler* s) {
h_.promise().sch = s;
}
};
scheduler g_sch;
task demo_task(int id) {
std::cout << "task " << id << " startn";
co_await std::suspend_always{};
std::cout << "task " << id << " resumedn";
co_return;
}
int main() {
auto t1 = demo_task(1);
t1.set_scheduler(&g_sch);
auto t2 = demo_task(2);
t2.set_scheduler(&g_sch);
g_sch.run();
return 0;
}
3.1 代码关键逻辑解析
在上面的代码中,demo_task返回一个task,其Promise的initial_suspend返回initial_awaiter,该awaiter在await_suspend中将协程句柄推入全局调度器。因此调用demo_task时协程并不会立即执行,而是进入就绪队列。
main函数中创建两个任务并设置调度器后,调用g_sch.run()启动循环。循环依次弹出句柄并resume,第一次恢复执行到co_await std::suspend_always{}再次挂起,控制权回到循环;第二个任务同理。由于示例未实现续跑逻辑,任务在首次挂起后结束于调度器视角,实际项目中可在final_suspend中重新入队或唤醒依赖协程。
四、co_await与Promise协作的注意事项
使用协程实现调度时,最容易出错的地方是协程帧的生命周期。若final_suspend不返回挂起awaiter,协程帧会在co_return后销毁,此时外部若仍持有句柄去resume会导致未定义行为。因此长时间调度的任务应在final_suspend挂起,由调度器显式销毁。
另一个要点是co_await的操作数必须满足Awaiter协议。标准库提供的std::suspend_always和std::suspend_never是最简单的实现,实际调度中常需自定义awaiter以在挂起时让出线程或注册回调,从而实现非阻塞等待。
4.1 性能与适用场景
相比回调函数,协程调度的额外开销主要来自协程帧的堆分配及恢复时的上下文切换,但远低于线程切换。它适合I/O密集、高并发且逻辑分支复杂的服务,如网络代理或游戏逻辑帧处理。对于计算密集型任务,协程并不能提供并行加速,仅改善代码可读性。
通过本文的示例,你可以看到C++20协程如何将异步流程扁平化。在真实项目中,可基于类似Promise结构扩展超时控制、优先级队列及跨线程调度,从而构建完整的协程运行时。