300行代码写一个协程库

300行代码写一个协程库
gogongxt代码在文末,参考自:https://github.com/cloudwu/coroutine
用 300 行 C 代码实现一个协程库:共享栈与 ucontext 的艺术
你写过这样的代码吗?一个网络服务器,每连接一个客户端就创建一个线程。当并发达到几千时,内存占用飙升,系统调用开销让你抓狂。你听说过协程——那些”轻量级线程”——但 C 语言没有原生的协程支持。
这篇文章会带你从零理解协程的原理,并通过一个真实的实现(约 300 行 C 代码)展示如何在 C 中实现高效的协程,包括一个关键技巧:共享栈,让你可以创建上千个协程而不耗尽内存。
协程是什么?为什么不是”用户态线程”那么简单
协程(Coroutine)常被描述为”用户态线程”或”轻量级线程”。这个比喻对了一半——协程确实在用户态切换,不需要内核参与。但另一半经常被忽略:协程的切换时机由程序员完全控制,而不是由调度器抢占。
这带来了几个关键区别:
| 特性 | 进程 | 线程 | 协程 |
|---|---|---|---|
| 切换发起者 | 内核调度器 | 内核调度器 | 程序员代码 |
| 切换时机 | 不可预测 | 不可预测 | 完全确定 |
| 是否需要锁 | 需要 | 需要 | 通常不需要 |
| 栈内存 | 独立,MB级 | 独立,KB-MB级 | 可共享 |
| 切换开销 | 高(~μs) | 中等(~μs) | 极低(~ns) |
这种”程序员控制切换”的特性意味着:在协程内部,你可以放心地操作全局数据结构,因为没有任何人会抢占你,除非你主动调用
yield() 让出控制权。
非对称协程:主从分明
协程分为两种:对称(Symmetric)和非对称(Asymmetric)。
对称协程中,所有协程平等,任何协程可以把控制权交给任何其他协程。
非对称协程中,有明确的”主调者”和”被调者”:
resume(co)—— 主调者唤醒协程 coyield()—— 被调者让出控制权,回到主调者
这就像函数调用:调用者调用被调用者,被调用者返回。Lua 的协程就是非对称的,而这个库也是。
非对称协程更易理解和使用,因为你只需要关心”我什么时候 yield”,而不需要决定”我要切换到谁”。
问题来了:协程的栈在哪?
每个执行流都需要栈来存储局部变量、返回地址、函数参数。线程有独立栈,协程呢?
方案一:每个协程独立栈
最简单的方案是给每个协程分配独立的栈空间。Go 语言的 goroutine 就是这样做的(初始 2KB,可增长)。
但 C 语言没有垃圾回收,栈增长复杂。通常做法是分配固定大小的栈,比如 8KB 或 64KB。问题:
1
2
3
4
void recursive_function(int depth) {
char buffer[1024]; // 在栈上分配 1KB
if (depth > 0) recursive_function(depth - 1); // 递归调用
}如果栈只有 8KB,这个函数递归几次就栈溢出了。更糟糕的是,你无法预知一个函数会用多少栈空间——它可能调用了一个你不知道的库函数。
方案二:共享栈 + 按需保存
这就是这个库采用的核心设计:
1
2
3
4
5
6
7
8
9
10
11
12
┌─────────────────────────────────────────────────────────┐
│ 共享栈 (1MB) │
│ ┌───────────────────────────────────────────────────┐ │
│ │ 所有协程共用 │ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ 协程 A 私有栈 │ │ 协程 B 私有栈 │ │ 协程 C 私有栈 │
│ (yield 时保存) │ │ (yield 时保存) │ │ (yield 时保存) │
│ ~50KB │ │ ~30KB │ │ ~10KB │
└──────────────────┘ └──────────────────┘ └──────────────────┘关键洞察:协程 yield 的那一刻,它实际使用的栈空间通常很小——几 KB 到几十 KB。只有在 yield 点需要保存栈,而不是一直占用。
代价是每次 resume/yield 需要复制栈数据,但协程切换通常不频繁,这个开销可控。
ucontext:POSIX 的用户态上下文切换
在 Linux/macOS 上,ucontext
系列函数提供了用户态上下文切换能力,使用了下面的一个头文件,一个结构体,三个函数:
1
2
3
4
5
6
7
8
9
10
11
12
#include <ucontext.h>
ucontext_t ctx; // 存储CPU状态:寄存器、栈指针、程序计数器等
// 获取当前上下文
getcontext(&ctx);
// 修改上下文,设置新的入口函数和栈
makecontext(&ctx, (void (*)(void))entry_func, argc, arg1, arg2, ...);
// 保存当前上下文到 old_ctx,然后跳转到 new_ctx
swapcontext(&old_ctx, &new_ctx);swapcontext 是核心:它保存当前的 CPU
状态(寄存器、程序计数器等)到第一个参数,然后从第二个参数恢复 CPU
状态,实现跳转。
一个关键细节:makecontext 需要先设置
uc_stack 指定栈空间,uc_link
指定退出后返回的上下文。
1
2
3
4
5
6
7
8
9
10
11
12
ucontext_t main_ctx, co_ctx;
char stack[1024*1024]; // 1MB 栈
getcontext(&co_ctx);
co_ctx.uc_stack.ss_sp = stack;
co_ctx.uc_stack.ss_size = sizeof(stack);
co_ctx.uc_link = &main_ctx; // 协程结束后回到 main_ctx
makecontext(&co_ctx, coroutine_entry, 0);
// 跳转到协程,协程结束后自动回到这里
swapcontext(&main_ctx, &co_ctx);核心数据结构
这个库只有两个核心结构体:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 协程状态
#define COROUTINE_DEAD 0 // 已结束
#define COROUTINE_READY 1 // 新建,未运行
#define COROUTINE_RUNNING 2 // 正在运行
#define COROUTINE_SUSPEND 3 // 已 yield,等待恢复
// 调度器:管理所有协程
struct schedule {
char stack[STACK_SIZE]; // 共享栈!1MB,所有协程共用
ucontext_t main; // 主上下文(resume/yield 的落脚点)
int nco; // 当前协程数
int cap; // 协程数组容量
int running; // 当前运行的协程 ID,-1 表示无
struct coroutine **co; // 协程指针数组
};
// 单个协程
struct coroutine {
coroutine_func func; // 协程函数
void *ud; // 用户数据
ucontext_t ctx; // 上下文(寄存器状态)
struct schedule *sch; // 所属调度器
ptrdiff_t cap; // 私有栈容量
ptrdiff_t size; // 私有栈实际大小
int status; // 状态
char *stack; // 私有栈(yield 时保存共享栈内容)
};注意 schedule.stack 是共享的,而
coroutine.stack 是每个协程私有的——只在 yield
时才有内容。
API 工作流程
创建调度器:coroutine_open()
1
struct schedule *S = coroutine_open();分配调度器结构体,初始化协程数组。共享栈 S->stack
在结构体内,1MB 大小。
创建协程:coroutine_new()
1
int co_id = coroutine_new(S, my_coroutine_func, user_data);分配 coroutine 结构体,设置状态为
READY。此时协程还没开始执行,只是分配了结构体。
唤起协程:coroutine_resume()
这是核心函数。根据协程状态有两种处理:
第一次 resume(状态 READY):
1
2
3
4
5
6
getcontext(&C->ctx); // 初始化上下文
C->ctx.uc_stack.ss_sp = S->stack; // 使用共享栈!
C->ctx.uc_stack.ss_size = STACK_SIZE;
C->ctx.uc_link = &S->main; // 结束后回到主流程
makecontext(&C->ctx, mainfunc, 2, ...); // 设置入口函数
swapcontext(&S->main, &C->ctx); // 跳转!后续 resume(状态 SUSPEND):
1
2
3
// 恢复:把保存的私有栈复制回共享栈
memcpy(S->stack + STACK_SIZE - C->size, C->stack, C->size);
swapcontext(&S->main, &C->ctx); // 跳转!让出协程:coroutine_yield()
1
2
3
4
5
6
7
8
9
10
void coroutine_yield(struct schedule *S) {
// 保存当前共享栈内容到私有栈
_save_stack(C, S->stack + STACK_SIZE);
C->status = COROUTINE_SUSPEND;
S->running = -1;
// 切回主流程
swapcontext(&C->ctx, &S->main);
}栈保存的技巧:_save_stack()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void _save_stack(struct coroutine *C, char *top) {
char dummy = 0; // 局部变量,在当前栈顶附近
// top 是共享栈底,&dummy 是当前栈顶
// 两者差值就是当前协程使用的栈大小
C->size = top - &dummy;
if (C->cap < C->size) {
free(C->stack);
C->stack = malloc(C->size); // 按需分配
C->cap = C->size;
}
memcpy(C->stack, &dummy, C->size); // 复制到私有栈
}这里的技巧是用一个局部变量 dummy
来探测当前栈指针位置。因为栈从高地址向低地址增长,top - &dummy
就是已使用的栈空间大小。
执行流程图解
用一个例子说明完整流程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void foo(struct schedule *S, void *ud) {
for (int i = 0; i < 3; i++) {
printf("iteration %d\n", i);
coroutine_yield(S); // 让出
}
}
int main() {
struct schedule *S = coroutine_open();
int co = coroutine_new(S, foo, NULL);
while (coroutine_status(S, co) != COROUTINE_DEAD) {
coroutine_resume(S, co);
}
coroutine_close(S);
return 0;
}执行流程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
时间线 ──────────────────────────────────────────────────────────►
main()
│
├─ coroutine_open()
│ └─ 创建调度器,共享栈初始化
│
├─ coroutine_new(S, foo, NULL)
│ └─ 创建协程结构体,状态 = READY
│
└─ 循环开始
│
├─ coroutine_resume(S, co) ─────────────────────┐
│ │ │
│ ├─ 状态是 READY │
│ ├─ 设置 ctx 使用共享栈 │
│ ├─ swapcontext(&S->main, &C->ctx) ──────► foo() 开始
│ │ │
│ │ 打印 "iteration 0"
│ │ coroutine_yield()
│ │ │
│ │ 保存栈到私有栈
│ │ 状态 = SUSPEND
│ │ swapcontext(&C->ctx, &S->main)
│ │ │
│ ◄───────────────────────────────────────────┘
│ (回到 main)
│
├─ coroutine_resume(S, co) ─────────────────────┐
│ │ │
│ ├─ 状态是 SUSPEND │
│ ├─ 恢复私有栈到共享栈 │
│ ├─ swapcontext(&S->main, &C->ctx) ──────► foo() 继续
│ │ │
│ │ 打印 "iteration 1"
│ │ coroutine_yield()
│ │ │
│ │ 保存栈到私有栈
│ │ 状态 = SUSPEND
│ │ swapcontext(&C->ctx, &S->main)
│ │ │
│ ◄───────────────────────────────────────────┘
│
├─ ... (iteration 2 类似)
│
└─ foo() 执行完毕
协程结构体被删除
状态 = DEAD
循环结束输出:
1
2
3
iteration 0
iteration 1
iteration 2两个协程交替执行
更复杂的例子——两个协程交替:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void foo(struct schedule *S, void *ud) {
int start = *(int*)ud;
for (int i = 0; i < 5; i++) {
printf("coroutine %d: %d\n", coroutine_running(S), start + i);
coroutine_yield(S);
}
}
int main() {
struct schedule *S = coroutine_open();
int arg1 = 0, arg2 = 100;
int co1 = coroutine_new(S, foo, &arg1);
int co2 = coroutine_new(S, foo, &arg2);
while (coroutine_status(S, co1) && coroutine_status(S, co2)) {
coroutine_resume(S, co1);
coroutine_resume(S, co2);
}
coroutine_close(S);
}输出:
1
2
3
4
5
6
7
coroutine 0: 0
coroutine 1: 100
coroutine 0: 1
coroutine 1: 101
coroutine 0: 2
coroutine 1: 102
...两个协程轮流执行,通过 yield 和 resume 实现协作式多任务。
权衡与限制
这个实现有其局限性:
优点:
- 内存效率极高:1000 个协程可能只占用几 MB(vs 独立栈需要 GB)
- 实现简单:约 300 行代码
- API 清晰:非对称协程模型易理解
缺点:
- 栈复制开销:每次 yield/resume 都要复制栈,不适合高频切换场景
- 单线程限制:一个调度器及其协程必须在同一线程内操作
- 平台限制:依赖
ucontext,Windows 需要用 Fiber API 替代
适用场景:
- 网络服务器(每个连接一个协程,在 IO 等待时 yield)
- 游戏逻辑(每个实体一个协程,等待事件时 yield)
- 状态机实现(用协程代替手动状态管理)
关键技术总结
| 技术 | 作用 |
|---|---|
ucontext |
用户态上下文切换,保存/恢复寄存器状态 |
| 共享栈 | 所有协程共用一个栈,减少内存占用 |
| 栈保存/恢复 | yield 时保存共享栈内容,resume 时恢复 |
| 栈指针探测 | 用局部变量地址计算栈使用量 |
扩展阅读
如果你对协程感兴趣,可以进一步了解:
- Lua 协程:这个实现的 API 设计参考了 Lua,文档见 https://www.lua.org/manual/5.4/manual.html#2.6
- Go goroutine:使用分段栈(segmented stack)或连续栈增长,文档见 https://go.dev/doc/effective_go#goroutines
- Boost.Context:C++ 的协程实现,支持 x86/ARM 汇编优化,仓库见 https://github.com/boostorg/context
- libco:微信后台使用的协程库,支持共享栈和独立栈两种模式,仓库见 https://github.com/Tencent/libco
源码
核心源码:coroutine_tutorial.c
编译运行:gcc -o coroutine_tutorial coroutine_tutorial.c && ./coroutine_tutorial
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
/**
* ============================================================================
* 用 300 行 C 代码实现一个协程库:共享栈与 ucontext 的艺术
* ============================================================================
*
* 本文件是一个完整的协程库实现,并添加了详细的中文注释,方便学习理解。
*
* 核心特性:
* 1. 使用 POSIX ucontext 实现用户态上下文切换
* 2. 采用共享栈设计,所有协程共用一个 1MB 栈空间
* 3. 非对称协程模型:resume/yield 配对使用
*
* 编译命令:gcc -o coroutine_tutorial coroutine_tutorial.c
* 运行命令:./coroutine_tutorial
*/
#include <assert.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* 平台兼容性处理:macOS 和 Linux 的 ucontext 头文件位置不同 */
#if __APPLE__ && __MACH__
#include <sys/ucontext.h>
#else
#include <ucontext.h>
#endif
/* ============================================================================
* 第一部分:常量定义
* ============================================================================
*/
/* 共享栈大小:1MB,所有协程共用这个栈空间 */
#define STACK_SIZE (1024 * 1024)
/* 协程数组默认容量 */
#define DEFAULT_COROUTINE 16
/* ============================================================================
* 第二部分:协程状态定义
* ============================================================================
*
* 协程有四种状态,形成一个生命周期:
*
* READY ──resume──> RUNNING ──yield──> SUSPEND
* │ │
* │ └──resume──> RUNNING
* │
* └──执行完毕──> DEAD
*/
#define COROUTINE_DEAD 0 /* 已结束:协程函数执行完毕 */
#define COROUTINE_READY 1 /* 就绪态:新创建,从未运行过 */
#define COROUTINE_RUNNING 2 /* 运行态:正在执行 */
#define COROUTINE_SUSPEND 3 /* 挂起态:已 yield,等待恢复 */
/* ============================================================================
* 第三部分:核心数据结构
* ============================================================================
*/
/* 前向声明 */
struct schedule;
/**
* 协程函数类型
*
* @param S 调度器指针,用于调用 yield 等 API
* @param ud 用户数据指针,在创建协程时传入
*
* 示例:
* void my_coroutine(struct schedule *S, void *ud) {
* int *data = (int *)ud;
* printf("数据: %d\n", *data);
* coroutine_yield(S); // 让出控制权
* }
*/
typedef void (*coroutine_func)(struct schedule* S, void* ud);
/**
* 单个协程结构体
*
* 每个协程都有独立的这个结构体来保存其状态,
* 但所有协程共享同一个栈空间(schedule.stack)。
*/
struct coroutine {
coroutine_func func; /* 协程入口函数 */
void* ud; /* 用户数据,传递给协程函数 */
ucontext_t ctx; /* 上下文:保存寄存器状态、栈指针等 */
struct schedule* sch; /* 所属调度器的指针 */
ptrdiff_t cap; /* 私有栈容量:已分配的字节数 */
ptrdiff_t size; /* 私有栈实际大小:当前使用的字节数 */
int status; /* 当前状态:READY/RUNNING/SUSPEND/DEAD */
char* stack; /* 私有栈:yield 时保存共享栈的内容 */
};
/**
* 调度器结构体
*
* 调度器是协程的容器和管理者,负责:
* 1. 维护共享栈空间
* 2. 管理所有协程的创建、调度和销毁
* 3. 记录当前运行的协程
*
* 注意:调度器不是线程安全的,所有操作必须在同一线程中进行。
*/
struct schedule {
char stack[STACK_SIZE]; /* 共享栈!所有协程共用这 1MB 空间 */
ucontext_t main; /* 主上下文:resume/yield 的跳转目标 */
int nco; /* 当前活跃的协程数量 */
int cap; /* 协程指针数组的容量 */
int running; /* 当前运行的协程 ID,-1 表示无 */
struct coroutine** co; /* 协程指针数组 */
};
/* ============================================================================
* 第四部分:内部辅助函数
* ============================================================================
*/
/**
* 创建一个新的协程结构体
*
* @param S 调度器指针
* @param func 协程入口函数
* @param ud 用户数据
* @return 新创建的协程指针
*
* 此时协程只是分配了结构体,还未开始执行,状态为 READY。
*/
static struct coroutine* _co_new(struct schedule* S, coroutine_func func,
void* ud) {
struct coroutine* co = malloc(sizeof(*co));
co->func = func;
co->ud = ud;
co->sch = S;
co->cap = 0;
co->size = 0;
co->status = COROUTINE_READY;
co->stack = NULL; /* 私有栈延迟分配,yield 时才需要 */
return co;
}
/**
* 删除协程结构体
*
* 释放协程的私有栈和结构体本身。
* 当协程函数执行完毕时调用。
*/
static void _co_delete(struct coroutine* co) {
free(co->stack);
free(co);
}
/**
* 保存协程的栈内容
*
* 这是共享栈设计的核心!当协程 yield 时,需要把当前使用的
* 共享栈内容保存到协程的私有栈中。
*
* @param C 协程指针
* @param top 共享栈的栈底地址(高地址)
*
* 技巧:用一个局部变量 dummy 来探测当前栈顶位置。
* 因为栈从高地址向低地址增长,所以:
* 栈使用量 = top - &dummy
*/
static void _save_stack(struct coroutine* C, char* top) {
/* dummy 是局部变量,位于当前栈顶附近 */
char dummy = 0;
/* 确保不会溢出共享栈 */
assert(top - &dummy <= STACK_SIZE);
/* 计算当前协程使用的栈大小 */
ptrdiff_t used_size = top - &dummy;
/* 如果私有栈容量不够,重新分配 */
if (C->cap < used_size) {
free(C->stack);
C->cap = used_size;
C->stack = malloc(C->cap);
}
/* 记录实际使用的栈大小 */
C->size = used_size;
/* 将共享栈的内容复制到私有栈 */
memcpy(C->stack, &dummy, C->size);
}
/**
* 协程入口包装函数
*
* 当协程第一次被 resume 时,会跳转到这个函数。
* 这个函数负责调用用户定义的协程函数,并在函数返回后清理资源。
*
* 参数传递技巧:由于 makecontext 只能传 int 参数,
* 我们将指针拆成两个 32 位整数传入,再组合起来。
*/
static void mainfunc(uint32_t low32, uint32_t hi32) {
/* 组合两个 32 位整数,恢复调度器指针 */
uintptr_t ptr = (uintptr_t)low32 | ((uintptr_t)hi32 << 32);
struct schedule* S = (struct schedule*)ptr;
/* 获取当前运行的协程 */
int id = S->running;
struct coroutine* C = S->co[id];
/* 执行用户定义的协程函数 */
C->func(S, C->ud);
/* 协程函数返回,协程结束,清理资源 */
_co_delete(C);
S->co[id] = NULL;
--S->nco;
S->running = -1;
/* 函数返回后,会自动跳转到 uc_link 指向的上下文(即 S->main) */
}
/* ============================================================================
* 第五部分:公开 API 实现
* ============================================================================
*/
/**
* 创建调度器
*
* @return 新创建的调度器指针
*
* 调度器创建后,可以往里面添加协程。
* 共享栈(1MB)在调度器结构体内部。
*/
struct schedule* coroutine_open(void) {
struct schedule* S = malloc(sizeof(*S));
S->nco = 0; /* 初始没有协程 */
S->cap = DEFAULT_COROUTINE; /* 初始容量 */
S->running = -1; /* 没有协程在运行 */
/* 分配协程指针数组 */
S->co = malloc(sizeof(struct coroutine*) * S->cap);
memset(S->co, 0, sizeof(struct coroutine*) * S->cap);
/* 注意:S->stack 是结构体成员,已经随 malloc 分配好了 1MB 空间 */
return S;
}
/**
* 关闭调度器
*
* 释放所有协程和调度器本身的内存。
*/
void coroutine_close(struct schedule* S) {
int i;
for (i = 0; i < S->cap; i++) {
struct coroutine* co = S->co[i];
if (co) {
_co_delete(co);
}
}
free(S->co);
S->co = NULL;
free(S);
}
/**
* 创建新协程
*
* @param S 调度器指针
* @param func 协程入口函数
* @param ud 用户数据(传递给协程函数)
* @return 协程 ID(非负整数)
*
* 协程创建后处于 READY 状态,需要调用 coroutine_resume 才会执行。
*/
int coroutine_new(struct schedule* S, coroutine_func func, void* ud) {
struct coroutine* co = _co_new(S, func, ud);
if (S->nco >= S->cap) {
/* 容量不够,需要扩容 */
int id = S->cap;
S->co = realloc(S->co, S->cap * 2 * sizeof(struct coroutine*));
memset(S->co + S->cap, 0, sizeof(struct coroutine*) * S->cap);
S->co[S->cap] = co;
S->cap *= 2;
++S->nco;
return id;
} else {
/* 在数组中找一个空位存放协程 */
int i;
for (i = 0; i < S->cap; i++) {
int id = (i + S->nco) % S->cap;
if (S->co[id] == NULL) {
S->co[id] = co;
++S->nco;
return id;
}
}
}
/* 不应该到达这里 */
assert(0);
return -1;
}
/**
* 恢复/启动协程
*
* @param S 调度器指针
* @param id 协程 ID
*
* 这是最核心的函数!根据协程状态有两种处理方式:
*
* 1. READY 状态(首次 resume):
* - 设置协程上下文,使用共享栈
* - 跳转到协程入口函数
*
* 2. SUSPEND 状态(后续 resume):
* - 恢复私有栈内容到共享栈
* - 跳转到协程上次 yield 的位置
*/
void coroutine_resume(struct schedule* S, int id) {
/* 确保当前没有其他协程在运行 */
assert(S->running == -1);
assert(id >= 0 && id < S->cap);
struct coroutine* C = S->co[id];
if (C == NULL) return;
int status = C->status;
switch (status) {
case COROUTINE_READY:
/* ========== 首次 resume:初始化协程上下文 ========== */
/* 获取当前上下文作为模板 */
getcontext(&C->ctx);
/* 关键!设置协程使用共享栈 */
C->ctx.uc_stack.ss_sp = S->stack;
C->ctx.uc_stack.ss_size = STACK_SIZE;
/* 设置协程结束后的返回位置:回到主流程 */
C->ctx.uc_link = &S->main;
/* 标记为运行态 */
S->running = id;
C->status = COROUTINE_RUNNING;
/* 设置协程入口函数
* 技巧:将调度器指针拆成两个 32 位整数传入
* 这是因为 makecontext 的参数类型限制 */
uintptr_t ptr = (uintptr_t)S;
makecontext(&C->ctx, (void (*)(void))mainfunc, 2, (uint32_t)ptr,
(uint32_t)(ptr >> 32));
/* 保存当前上下文到 S->main,跳转到 C->ctx
* 协程开始执行! */
swapcontext(&S->main, &C->ctx);
break;
case COROUTINE_SUSPEND:
/* ========== 恢复 yield 的协程 ========== */
/* 关键!将保存的私有栈内容恢复到共享栈 */
memcpy(S->stack + STACK_SIZE - C->size, C->stack, C->size);
/* 标记为运行态 */
S->running = id;
C->status = COROUTINE_RUNNING;
/* 保存当前上下文到 S->main,跳转到 C->ctx
* 协程从 yield 点继续执行! */
swapcontext(&S->main, &C->ctx);
break;
default:
/* 不应该出现其他状态 */
assert(0);
}
}
/**
* 让出协程
*
* @param S 调度器指针
*
* 协程主动让出控制权,回到调用 resume 的位置。
* 当前协程的栈内容会被保存到私有栈。
*/
void coroutine_yield(struct schedule* S) {
int id = S->running;
assert(id >= 0);
struct coroutine* C = S->co[id];
/* 确保协程的栈在共享栈范围内 */
assert((char*)&C > S->stack);
/* 关键!保存当前共享栈内容到私有栈 */
_save_stack(C, S->stack + STACK_SIZE);
/* 更新状态 */
C->status = COROUTINE_SUSPEND;
S->running = -1;
/* 保存当前上下文到 C->ctx,跳转到 S->main
* 控制权回到调用 resume 的位置! */
swapcontext(&C->ctx, &S->main);
}
/**
* 查询协程状态
*
* @param S 调度器指针
* @param id 协程 ID
* @return 协程状态(COROUTINE_DEAD/READY/RUNNING/SUSPEND)
*/
int coroutine_status(struct schedule* S, int id) {
assert(id >= 0 && id < S->cap);
if (S->co[id] == NULL) {
return COROUTINE_DEAD;
}
return S->co[id]->status;
}
/**
* 获取当前运行的协程 ID
*
* @param S 调度器指针
* @return 当前运行的协程 ID,-1 表示没有协程在运行
*
* 这个函数可以在协程内部调用来获取自己的 ID。
*/
int coroutine_running(struct schedule* S) { return S->running; }
/* ============================================================================
* 第六部分:示例程序
* ============================================================================
*
* 下面的示例展示了如何使用这个协程库:
* 1. 创建调度器
* 2. 创建两个协程
* 3. 轮流恢复两个协程,直到它们都结束
* 4. 关闭调度器
*/
/* 用户数据结构 */
struct args {
int n;
};
/**
* 示例协程函数
*
* 这个协程会打印 5 次消息,每次打印后 yield 让出控制权。
*/
static void foo(struct schedule* S, void* ud) {
struct args* arg = ud;
int start = arg->n;
int i;
for (i = 0; i < 5; i++) {
/* 打印当前协程 ID 和计数值 */
printf("coroutine %d : %d\n", coroutine_running(S), start + i);
/* 让出控制权,等待下次 resume */
coroutine_yield(S);
}
/* 函数返回后,协程自动结束(状态变为 DEAD) */
}
/**
* 测试函数:演示协程的交替执行
*/
static void test(struct schedule* S) {
struct args arg1 = {0}; /* 协程 1 的参数,从 0 开始计数 */
struct args arg2 = {100}; /* 协程 2 的参数,从 100 开始计数 */
/* 创建两个协程 */
int co1 = coroutine_new(S, foo, &arg1);
int co2 = coroutine_new(S, foo, &arg2);
printf("main start\n");
/* 轮流恢复两个协程,直到它们都结束
* 应该分别检查每个协程的状态 */
while (coroutine_status(S, co1) || coroutine_status(S, co2)) {
if (coroutine_status(S, co1)) {
coroutine_resume(S, co1); /* 恢复协程 1 */
}
if (coroutine_status(S, co2)) {
coroutine_resume(S, co2); /* 恢复协程 2 */
}
}
printf("main end\n");
}
/**
* 主函数
*/
int main() {
/* 创建调度器 */
struct schedule* S = coroutine_open();
/* 运行测试 */
test(S);
/* 关闭调度器 */
coroutine_close(S);
return 0;
}
/* ============================================================================
* 预期输出:
*
* main start
* coroutine 0 : 0
* coroutine 1 : 100
* coroutine 0 : 1
* coroutine 1 : 101
* coroutine 0 : 2
* coroutine 1 : 102
* coroutine 0 : 3
* coroutine 1 : 103
* coroutine 0 : 4
* coroutine 1 : 104
* main end
*
* 可以看到两个协程交替执行,每次 yield 后回到主流程,
* 然后 resume 另一个协程。
* ============================================================================
*/



