Coroutine 函数手册
C++ 后端服务器框架Coroutine 函数手册
CHAPTER 15

Coroutine 函数手册

9 分钟阅读 · 2,223 字

依据源码:include/Coroutine.h、src/Coroutine.cpp。

本文按「符号 → 签名 → 作用 → 执行步骤 → 前置/后置条件 → 平台差异 → 边界与面试点」逐条说明。


0. 全局常量与线程局部变量

constexpr size_t kDefaultStackSize(Coroutine.cpp)

  • 值:128 * 1024(128 KiB)
  • 用途:协程栈大小(Linux ucontext 栈、CreateFiber 栈参数)

线程局部(TLS)变量(Coroutine.cpp 定义,Coroutine.h 声明)

符号 类型 含义
tls_current_coro Coroutine* 当前正在运行的协程;resume 入口设置,suspend 返回调度器后可能被清空
tls_current_scheduler Scheduler* 当前 worker 线程关联的调度器实例
tls_current_worker SchedulerWorker* 当前线程对应的 worker 结构(本地队列、抢占标记)
tls_scheduler_ctx ucontext_t 仅 Linux:调度器侧上下文,swapcontext 的另一端
tls_scheduler_fiber LPVOID 仅 Windows:由 ConvertThreadToFiber 得到的“调度器 fiber”

面试点:协程切换依赖 TLS;跨线程 resume 同一 Coroutine 在本实现中未做约束,设计上应在固定 worker 线程上驱动。


1. struct SchedulerWorker

定义位置:include/Coroutine.h

成员 含义
th 该 worker 的 std::thread
localQueue 本 worker 优先消费的协程队列(deque<shared_ptr<Coroutine>>)
localMtx 保护 localQueue
preemptRequested 抢占标记;preemptLoop 周期性置 true,shouldPreempt 读并清零

2. enum class CoroutineState

值 含义
READY 已创建,尚未首次 resume 跑完或刚创建
RUNNING resume 中,已进入协程体
SUSPEND suspend 后挂起,可再次 resume
DONE 协程函数执行完毕;resume 会直接返回

3. 类 Coroutine

3.1 explicit Coroutine(Func func)

  • 签名:Coroutine::Coroutine(Func func),Func = std::function<void()>
  • 文件:src/Coroutine.cpp L34–50
  • 作用:构造协程对象并初始化平台相关的上下文与入口。

Linux(CBSF_USE_UCONTEXT)步骤:

  1. state_ = READY
  2. 分配 stack_,大小 kDefaultStackSize
  3. getcontext(&ctx_),失败抛 runtime_error("getcontext failed")
  4. 设置 ctx_.uc_stack 指向栈与大小,uc_link = nullptr
  5. makecontext(&ctx_, entry, 1, this),入口为静态 entry

Windows(CBSF_USE_FIBER)步骤:

  1. CreateFiber(kDefaultStackSize, fiberEntry, this)
  2. 失败抛 runtime_error("CreateFiber failed")

异常:构造失败抛 std::runtime_error。

面试点:栈固定 128KB,深递归或大局部数组可能栈溢出。


3.2 ~Coroutine()

  • 文件:src/Coroutine.cpp L52–56
  • 作用:释放 Windows Fiber(DeleteFiber)。
  • Linux:析构中不释放 ucontext 资源(栈由 unique_ptr 管理);无额外系统调用。

3.3 Coroutine(Coroutine&& other) noexcept

  • 文件:src/Coroutine.cpp L58–73
  • 作用:移动构造;转移 func_、状态与平台上下文/栈。
  • 副作用:other.state_ = DONE;Windows 上 other.fiber_ = nullptr,避免双重 DeleteFiber。

注意:移动后原对象处于“不可用/已完成”语义,避免再 resume。


3.4 void resume()

  • 文件:src/Coroutine.cpp L75–90
  • 作用:从当前调度器上下文切换到本协程上下文并开始/继续执行。

前置条件(隐含):

  • state_ != DONE,否则直接返回。
  • tls_current_scheduler != nullptr,否则直接返回(无法切回调度器)。

步骤:

  1. state_ = RUNNING
  2. tls_current_coro = this
  3. Linux:swapcontext(&tls_scheduler_ctx, &ctx_)
  4. Windows:SwitchToFiber(fiber_)
  5. 从 suspend 返回后:若 tls_current_coro == this,则置 tls_current_coro = nullptr

面试点:

  • resume 必须在已设置 tls_current_scheduler 的线程中调用(通常即 run_on_worker_thread)。
  • Linux 路径依赖 tls_scheduler_ctx 在首次 swapcontext 前已处于合法状态(见下文「实现注意」)。

3.5 void suspend()

  • 文件:src/Coroutine.cpp L92–106
  • 作用:从协程切回调度器上下文。

前置条件:tls_current_scheduler != nullptr,否则直接返回。

步骤:

  1. 若 state_ == RUNNING,则改为 SUSPEND
  2. Linux:swapcontext(&ctx_, &tls_scheduler_ctx)
  3. Windows:若 tls_scheduler_fiber 非空,则 SwitchToFiber(tls_scheduler_fiber)

面试点:DONE 后 entry 仍会调用 suspend(),此时 state_ 已是 DONE,suspend 不会把状态改回 SUSPEND,但仍会切回调度器 fiber。


3.6 CoroutineState state() const

  • 文件:include/Coroutine.h L65
  • 作用:返回当前状态枚举。

3.7 static void Coroutine::entry(intptr_t arg)(Linux,私有)

  • 文件:src/Coroutine.cpp L15–22
  • 作用:makecontext 的 C 风格入口;arg 为 this 指针。

步骤:

  1. coro = reinterpret_cast<Coroutine*>(arg)
  2. 若 coro->func_ 非空则调用
  3. coro->state_ = DONE
  4. coro->suspend() 切回调度器

3.8 static void WINAPI Coroutine::fiberEntry(LPVOID lpParameter)(Windows,私有)

  • 文件:src/Coroutine.cpp L24–31
  • 作用:与 entry 等价,Fiber 回调形式。

4. 类 Scheduler

4.1 static Scheduler& instance()

  • 文件:include/Coroutine.h L88–91
  • 作用:Meyers 单例,进程内唯一 Scheduler。

4.2 template<typename F, typename... Args> void submit(F&& f, Args&&... args)

  • 文件:include/Coroutine.h L93–97
  • 作用:将可调用对象打包为 std::function<void()>,封装进 Coroutine,提交到 submitTask。

实现细节:

  • std::bind(std::forward<F>(f), std::forward<Args>(args)...)
  • submitTask(std::make_shared<Coroutine>(std::move(task)))

面试点:std::bind 对重载、引用包装等边界需注意;与 ThreadPool 头里“无参不用 bind”的注释是不同文件的选择。


4.3 template<typename F, typename... Args> void go(F&& f, Args&&... args)

  • 文件:include/Coroutine.h L99–102
  • 作用:submit 的别名。

4.4 void submitTask(std::shared_ptr<Coroutine> coro)(私有)

  • 文件:src/Coroutine.cpp L109–126
  • 作用:入队一个协程任务并唤醒 worker。

步骤:

  1. 若 !running_.load(),调用 start(1) 隐式启动调度器
  2. 若当前线程 TLS:tls_current_scheduler == this 且 tls_current_worker 非空
    → 加锁 tls_current_worker->localMtx,localQueue.emplace_back(move(coro))
  3. 否则
    → 加锁 globalMtx_,globalQueue_.emplace_back(move(coro))
  4. cv_.notify_one()

面试点:同调度器实例上,在 worker 内 submit 会优先进本地队列,有利于局部性。


4.5 std::shared_ptr<Coroutine> popLocalTask(SchedulerWorker& worker)(私有)

  • 文件:src/Coroutine.cpp L128–134
  • 作用:从指定 worker 的本地队列队首弹出一个协程;空则 nullptr。

4.6 std::shared_ptr<Coroutine> popGlobalTask()(私有)

  • 文件:src/Coroutine.cpp L136–142
  • 作用:从全局队列队首弹出;空则 nullptr。

4.7 std::shared_ptr<Coroutine> stealTask(SchedulerWorker& self)(私有)

  • 文件:src/Coroutine.cpp L144–154
  • 作用:工作窃取:遍历其他 worker,从其本地队列队尾偷一个任务。

面试点:队首本地、队尾窃取是常见策略,减少与本地 pop 的冲突。


4.8 void start(size_t workerCount = hardware_concurrency())

  • 文件:src/Coroutine.cpp L156–180
  • 作用:启动调度器:创建 workerCount 个 worker 线程 + 1 个抢占线程。

步骤:

  1. workerCount == 0 时改为 1
  2. running_.compare_exchange_strong(false, true),已为 true 则直接返回(幂等)
  3. workers_.clear(),预分配,逐个 make_unique<SchedulerWorker>()
  4. 每个 worker:w->th = thread{ [this,w]{ tls_current_worker=w; run_on_worker_thread(); tls_current_worker=nullptr; } }
  5. preemptThread_ = thread{ [this]{ preemptLoop(); } }

异常:run_on_worker_thread 内 Windows 上 ConvertThreadToFiber 失败会抛异常(见 4.12)。


4.9 void stop()

  • 文件:src/Coroutine.cpp L182–201
  • 作用:停止调度器并回收线程。

步骤:

  1. running_.compare_exchange_strong(true, false),已为 false 则直接返回
  2. cv_.notify_all() 唤醒等待中的 worker
  3. join preemptThread_
  4. 对每个 worker join th
  5. 加锁清空 globalQueue_
  6. workers_.clear()

注意:析构 ~Scheduler() 会调用 stop(),确保进程退出时线程收敛。


4.10 void configureTimeSlice(std::chrono::milliseconds slice)

  • 文件:src/Coroutine.cpp L203–206
  • 作用:设置 preemptLoop 的睡眠间隔;slice.count() <= 0 则忽略。

4.11 bool shouldPreempt() const

  • 文件:src/Coroutine.cpp L208–211
  • 作用:若当前 TLS 属于本调度器且 tls_current_worker 有效,则原子地读取并清除该 worker 的 preemptRequested。

返回:是否需要让出(给 safe_point 用)。


4.12 void run_on_worker_thread()

  • 文件:src/Coroutine.cpp L222–277
  • 作用:worker 线程主循环:取任务、resume、未结束则重新入本地队列。

步骤概要:

  1. tls_current_scheduler = this
  2. Windows:tls_scheduler_fiber = ConvertThreadToFiber(nullptr),失败抛 runtime_error("ConvertThreadToFiber failed")
  3. 若 tls_current_worker == nullptr,则指向栈上 fallbackWorker(兼容非标准调用路径)
  4. 循环:
    • coro = popLocal → popGlobal → steal
    • 若无任务:unique_lock(globalMtx_),sleepingWorkers_++,cv_.wait 直到 !running 或全局队列非空,sleepingWorkers_--;若 !running 则 break
    • 若有任务且状态为 READY 或 SUSPEND:调用 coro->resume()
    • 若 coro->state() != DONE:锁本地队列 push_back,notify_one
  5. 退出循环后:Windows ConvertFiberToThread() 清理 fiber
  6. 清空 tls_current_coro、tls_current_scheduler;若用过 fallbackWorker 则清空 tls_current_worker

面试点:

  • 未完成协程重新入本地队列,而不是全局队列。
  • 等待条件仅用 globalQueue_.empty(),本地队列有任务时不会通过 cv 唤醒(依赖正在运行的 worker 自己处理;窃取者依赖其他 worker 活跃)。

4.13 void preemptLoop()(私有)

  • 文件:src/Coroutine.cpp L213–220
  • 作用:独立线程循环:每 timeSlice_ 睡眠一次,给所有 worker 的 preemptRequested 置 true。

面试点:这是协作式抢占提示,不是操作系统抢占;协程需 safe_point/yield 才真正让出。


5. 自由函数

5.1 inline void yield() / inline void yield_now()

  • 文件:include/Coroutine.h L131–137
  • 作用:若 tls_current_coro 非空,调用其 suspend()。

区别:当前二者实现完全相同。


5.2 void safe_point()

  • 文件:src/Coroutine.cpp L279–285
  • 作用:若当前在调度器上且存在当前协程,并且 shouldPreempt() 为真,则 yield()。

5.3 void sleep_for(std::chrono::milliseconds dur)

  • 文件:src/Coroutine.cpp L287–290
  • 作用:std::this_thread::sleep_for(dur) 后调用 safe_point()。

面试点:这会阻塞整个 OS 线程,不是“只挂起当前协程”;长睡眠会占用 worker。


6. 实现注意(Linux ucontext 与代码审阅)

在 resume/suspend 中使用 swapcontext(&tls_scheduler_ctx, &ctx_) 与 swapcontext(&ctx_, &tls_scheduler_ctx)。

标准用法通常需要在 worker 线程首次切入协程前,对 tls_scheduler_ctx 执行一次 getcontext,以便 swapcontext 有合法的“调度器保存点”。当前 run_on_worker_thread 的 Linux 分支未展示该初始化(与 Windows 显式 ConvertThreadToFiber 不对称)。

文档建议:若在 Linux 上实际跑通,需对照你本地是否另有补丁或编译宏;面试时可答:“ucontext 路径需要保证 scheduler 上下文已 getcontext 引导,否则首次切换行为未定义。”


7. 与 Server 的调用关系(便于串联记忆)

Server::run 中会:

  • Scheduler::instance().configureTimeSlice(...)
  • Scheduler::instance().start(...)

HTTP 连接处理仍主要走 ThreadPool::handleClient;协程调度器在旁路运行,业务协程需自行 submit 使用。


下一模块若要同样粒度,可直接说模块名(例如 Server 或 Threadpool),我按本文件结构继续写 docs/modules/detail/<Module>_FUNCTIONS.md。

全部章节