如何用 C 实现协程
写在前面
本文是针对南大蒋炎岩老师 M2: 协程库 的实现总结。如果你也正在做这个 lab,请退出本文,仔细阅读实验文档,多写多调,虽然很难很痛苦,但是真的很有意思。如果你已经完成,欢迎邮件联系讨论相关内容,包括实现思路和相关代码交流。
关于这个实验
从开始阅读文档,到迄今为止完成 64bit 的测试用例(是的,32bit 测试用例还没过),耗费了大量的时间,一点一点摸索。揪着头发在脑子里运行协程,用 GDB 一行一行调试代码,逼急了甚至逐步过汇编代码,无时不让我感慨 coding 和 debug 真难,也真有意思。
2022-07-25 update:
使劲折腾终于用一种无语的方法把 32bit 测试用例通过了,此刻心情复杂。
协程
经过这段时间的 coding,让我大概对 协程 有了一些理解。
线程是 CPU 调度的最小单位,通过系统调用,陷入内核态来完成线程管理和调度;相比较进程而言,调度时上下文更少,因此开销更小一些。然而,抢占式调度终究还要看操作系统的脸色,陷入内核态还是会带来较大开销。
协程,我认为就是一种协作式调度的用户级线程。协作式,也就是由用户来决定调度时机,而不像线程用完时间片就被剥夺 CPU;用户级线程,无需陷入内核态,只要在用户态就可以完成逻辑流的切换。
除此之外,Linux 的线程栈大小默认为 8MB,一般来说用不到这么大;而协程可以自定义栈大小(可以通过修改系统文件来分配线程栈),相比较之下可以起更多的协程。
对于 I/O 密集型任务而言,CPU 用的少,需要更多的进程 or 线程来使用空闲的 CPU,提高利用率。协程相较于进程 or 线程而言,切换上下文开销更少,协程栈更小,就能开更多的协程来执行任务。
libco
libco 定义了相关的 API:
1 | struct co *co_start(const char *name, void (*func)(void *), void *arg); |
co_start()负责创建一个协程,并指定任务函数及参数,创建好的协程不会直接运行,而是返回其指针等待用户指令;co_yield()负责在协程池中随机挑选一个待运行的协程,也就是『协作式调度』的切换函数;co_wait()暂停当前协程,直到指定协程运行结束后再继续当前协程,会在指定协程运行结束后释放其资源,所以用户应当保证初始协程外的每个协程被此函数执行一次。
main 函数所在的协程即为初始协程,通过调用co_yield()和co_wait()切换其他协程,并根据指定的任务函数和参数开始执行,直到函数返回 or 再次切换。
不论还有多少协程未执行结束,只要 mian 函数结束,整个进程即终止。
co 与协程池
定义协程,指定其协程栈大小为STACK_SIZE,64KB。
1 |