悠悠楠杉
C++手写协程实现原理与示例
什么是协程?
协程(Coroutine)是一种比线程更轻量的并发编程模型,它允许函数在执行过程中被挂起,并在之后从挂起点继续执行。与多线程不同,协程是协作式的——它们不会被操作系统强制调度,而是由程序员显式控制何时让出执行权。这种机制特别适合高并发IO密集型场景,如网络服务器、异步任务处理等。
虽然C++20引入了标准协程支持,但理解其底层实现原理对深入掌握并发编程至关重要。本文将带你从零开始,用C语言风格的C++实现一个极简的协程库,帮助你理解协程的核心机制。
协程的核心:上下文切换
协程的本质在于“保存和恢复执行上下文”。所谓上下文,就是程序运行时的状态,包括栈指针、指令指针、寄存器值等。要实现协程切换,关键在于如何捕获当前执行状态,并在将来某个时刻精确地恢复它。
在POSIX系统中,setjmp 和 longjmp 提供了这样的能力。setjmp 用于保存当前执行环境到一个jmp_buf结构中,而longjmp则可以从该结构恢复执行环境,使程序跳转回setjmp调用点,并让setjmp返回指定的非零值。
这正是我们构建协程的基础工具。
极简协程库实现
下面是一个基于setjmp/longjmp的协程实现框架:
cpp
include
include
include
include <setjmp.h>
// 协程最大数量
const int MAXCOROUTINES = 16;
// 每个协程栈大小(字节)
const int STACKSIZE = 8192;
struct Coroutine {
char* stack; // 栈空间
jmp_buf context; // 上下文
void (*func)(); // 协程函数
bool finished; // 是否执行完毕
};
Coroutine coroutines[MAXCOROUTINES]; int current = 0; // 当前运行的协程ID jmpbuf main_context; // 主协程上下文
// 切换到指定协程
void switch_to(int cid) {
if (setjmp(coroutines[current].context) == 0) {
current = cid;
longjmp(coroutines[cid].context, 1);
}
}
// 协程入口函数包装
void coroutine_entry() {
coroutines[current].func();
coroutines[current].finished = true;
// 执行完后切回主协程
longjmp(main_context, 1);
}
// 创建协程
int createcoroutine(void (*func)()) {
static int nextid = 0;
int id = next_id++;
Coroutine& co = coroutines[id];
co.func = func;
co.finished = false;
co.stack = new char[STACK_SIZE];
// 设置栈顶指针并初始化上下文
char* stack_top = co.stack + STACK_SIZE - sizeof(void*);
// 使用汇编或平台相关方式设置栈指针(简化版)
// 实际中需确保在新栈上调用setjmp
if (setjmp(co.context) == 0) {
// 模拟跳转到新栈执行
// 注意:此为教学简化,生产环境需更严谨的栈切换
char fake;
ptrdiff_t offset = &fake - stack_top;
if (offset > 0 && offset < STACK_SIZE) {
// 这里应通过汇编真正切换栈,此处略
coroutine_entry();
}
}
return id;
}
上述代码展示了协程的基本结构。每个协程拥有独立的栈空间和上下文缓冲区。创建时通过setjmp保存初始状态,当协程首次被调度时,会跳转到coroutine_entry执行用户函数。
调度与协作
协程之间的切换必须由程序员主动触发。我们可以定义一个yield()函数来实现让出执行权:
cpp
void yield() {
int next = (current + 1) % MAX_COROUTINES;
while (coroutines[next].finished && next != current) {
next = (next + 1) % MAX_COROUTINES;
}
if (!coroutines[next].finished) {
switch_to(next);
}
}
主循环负责启动所有协程并进行调度:
cpp
void run_all() {
if (setjmp(main_context) == 0) {
for (int i = 0; i < MAX_COROUTINES; ++i) {
if (!coroutines[i].finished && coroutines[i].func) {
switch_to(i);
}
}
}
}
使用示例
cpp
void task1() {
for (int i = 0; i < 3; ++i) {
std::cout << "Task 1 - Step " << i << std::endl;
yield();
}
}
void task2() {
for (int i = 0; i < 3; ++i) {
std::cout << "Task 2 - Step " << i << std::endl;
yield();
}
}
int main() {
createcoroutine(task1);
createcoroutine(task2);
run_all();
return 0;
}
输出将是两个任务交替执行,展现出协作式多任务的效果。
总结与思考
这个简易协程库虽然功能有限,但它揭示了协程的本质:通过保存和恢复执行上下文实现函数间的非抢占式切换。真正的生产级协程库(如Boost.Context)会使用内联汇编精确控制ESP/RSP寄存器,确保跨平台稳定性和性能。
值得注意的是,这种手动管理栈的方式存在诸多限制,比如无法在协程中使用局部变量地址传递给外部,且调试困难。现代C++20协程通过编译器支持解决了这些问题,但理解底层机制依然是成为高级C++开发者的必经之路。

