协程本质是控制流的主动挂起与恢复,需栈、寄存器上下文和调度逻辑;ucontext.h仅用于教学演示,Boost.Context提供跨平台、异常安全的工业级实现,C++20协程则为无栈状态机方案。

用 C++ 实现一个“简单”的协程库,核心不是从零造轮子(比如手写汇编切换栈),而是理解协程的本质:**控制流的主动挂起与恢复**,并借助系统或标准库提供的上下文切换原语。Boost.Context 是成熟方案,而 ucontext.h(POSIX)是更底层、可教学但已废弃的接口。下面直击重点,不绕弯。
一、协程的本质:栈 + 寄存器上下文 + 调度逻辑
协程不是线程,它不抢占、不依赖内核调度。一次协程切换 = 保存当前函数执行现场(栈指针、指令指针、通用寄存器等)→ 跳转到另一段栈上的函数继续执行。关键不在“多任务”,而在“可控让出”(yield)和“可控唤醒”(resume)。
所以真正要实现的最小闭环是:
- 一个能分配独立栈内存的对象(如
char stack[8192]) - 一套能保存/恢复 CPU 上下文的机制(这就是
ucontext或boost::context::continuation干的事) - 一个状态机管理协程生命周期(created → ready → running → suspended → done)
二、用 ucontext.h 写一个极简协程(仅作原理演示)
注意:ucontext 已被 POSIX 标准弃用,glibc 2.26+ 默认禁用,仅用于理解底层。生产环境请用 Boost.Context 或 C++20 协程。
立即学习“C++免费学习笔记(深入)”;
下面是去掉错误检查、仅保留主干的可运行片段:
#include <ucontext.h>
#include <iostream>
#include <vector>
<p>struct SimpleCoro {
ucontext_t ctx;
char stack[8192];
bool is_done = false;</p><pre class="brush:php;toolbar:false;">SimpleCoro(std::function<void()> f) {
getcontext(&ctx);
ctx.uc_stack.ss_sp = stack;
ctx.uc_stack.ss_size = sizeof(stack);
ctx.uc_link = nullptr; // 挂起后返回到哪?这里设为 null,由我们手动 resume 控制
makecontext(&ctx, [](int) {
// 这里是协程体入口,需转成 C 风格函数
auto* self = reinterpret_cast<SimpleCoro*>(reinterpret_cast<char*>(&self) - sizeof(self));
self->body();
}, 1, reinterpret_cast<int>(&this));
}
void body() {
// 用户逻辑在此运行
std::cout << "coro running\n";
// 模拟 yield:切回主协程(需提前保存主 ctx)
swapcontext(&ctx, &main_ctx); // ⚠️ main_ctx 需在 main 中 setcontext 前 getcontext
std::cout << "coro resumed\n";
is_done = true;
}
void resume() {
if (!is_done) swapcontext(&main_ctx, &ctx);
}};
ucontext_t main_ctx; // 全局保存主上下文(不推荐,仅示意)
int main() { getcontext(&main_ctx); SimpleCoro c([]{}); c.resume(); // 启动协程 std::cout
⚠️ 缺陷明显:无法传递参数、无异常安全、栈大小固定、makecontext 参数传递靠 hack 地址、不可重入。这正是 Boost.Context 存在的理由。
三、用 Boost.Context 实现可工业使用的轻量协程
Boost.Context 封装了平台差异(x86/x64/ARM,Windows Fibers / Linux setjmp / macOS),提供 RAII、异常传播、栈分配策略。最简可用示例:
#include <boost/context/all.hpp>
#include <iostream>
<p>namespace ctx = boost::context;</p><p>struct Task {
ctx::continuation c<em>;
bool done</em> = false;</p><pre class="brush:php;toolbar:false;">Task(std::function<void(ctx::continuation&)> fn)
: c_(ctx::callcc([fn = std::move(fn)](ctx::continuation&& c) {
fn(c); // 用户函数接收 continuation,可用来 yield
return std::move(c);
})) {}
void resume() {
if (!done_) c_ = std::move(c_).resume();
done_ = c_.fpc_ == nullptr; // 判断是否已结束(内部指针为空)
}
void yield() {
c_ = std::move(c_).resume(); // 切回父协程(即构造时 callcc 的调用方)
}};
// 使用示例 int main() { Task t([](ctx::continuation& c) { std::cout
std::cout << "before first resume\n"; t.resume(); // 启动 std::cout << "after first resume\n"; t.resume(); // 继续
}
优势一目了然:
-
continuation是移动语义安全的对象,支持栈上/堆上分配 -
callcc(call-with-current-continuation)天然支持双向跳转 - 可指定栈大小、栈分配器(如
fixedsize_stack或protected_fixedsize_stack) - 异常会正常传播(不像 ucontext 可能崩溃)
四、为什么不直接手写汇编?以及 C++20 协程怎么选?
手写汇编(如 x86 的 push/pop 所有寄存器 + 修改 rsp/rip)理论上可行,但:
- 不同 ABI(System V / Win64)、不同架构(x86-64 / AArch64)寄存器列表和调用约定完全不同
- 编译器优化(如 RVO、尾调用)可能破坏你预设的栈帧布局
- 调试困难、无法和现代 C++ 特性(RAII、异常、lambda 捕获)自然融合
C++20 协程是语言级支持,语法简洁(co_await, co_yield, co_return),但它是无栈协程(stackless),依赖编译器生成状态机,不能像 Boost.Context 那样自由跳转任意函数。适合 I/O 等待场景,不适合需要完整调用栈的协作式多任务(如游戏逻辑帧、解释器)。
结论:学原理看 ucontext;做项目用 Boost.Context;新项目且场景匹配,优先评估 C++20 coroutines + libunifex 或 cppcoro。
基本上就这些。协程不神秘,难的是跨平台健壮性和与现代 C++ 生态的无缝集成——Boost.Context 已经替你扛住了。











