在之前的文章中,对于多线程程序在操作系统中的调度问题已经做了一定的介绍,详见:
https://blog.csdn.net/kakaweb/article/details/102214586
当处于running状态的线程进行系统调用、阻塞io、获取锁等操作时,将进入blocking状态,将cpu core出让给其它处于runnable状态的线程,这一过程被称为上下文切换,需要保存stack、register、pc register、sp register等线程运行时现场,同时载入新线程所需的信息。在此过程中,需要消耗大量的cpu执行时间(12k-18k条指令),所以程序的运行效率无法通过增加线程数目线性提高。
由上述分析可知,通过增加线程数提升运行效率的主要障碍在于上下文切换时的巨大消耗,这也是linux中由内核负责调度线程带来的必然缺陷。那么,能否将调度在用户态完成从而减少消耗呢?这时就需要引入协程(coroutine)的概念了。
“协程”(Coroutine)概念最早由 Melvin Conway 于1958年提出。协程可以理解为纯用户态的线程,其通过协作而不是抢占来进行切换。相对于进程或者线程,协程所有的操作都可以在用户态完成,创建和切换的消耗更低。总的来说,协程为协同任务提供了一种运行时抽象,这种抽象非常适合于协同多任务调度和数据流处理。在现代操作系统和编程语言中,因为用户态线程切换代价比内核态线程小,协程成为了一种轻量级的多任务模型。
从编程角度上看,协程的思想本质上就是控制流的主动让出(yield)和恢复(resume)机制,迭代器常被用来实现协程,所以大部分的语言实现的协程中都有yield关键字,比如Python、PHP、Lua。但也有特殊比如Go就使用的是通道来通信。
从上面的定义中可以知道,协程高效运行的秘密在于在用户态完成调度工作(非抢占式),减少用户态至内核态的切换。此处我们可以将协程的调度切换为几个基本的要素:调度器、协程yield、协程resume。
基于ucontext实现协程调度
ucontext是GNU C库提供的一组创建、保存、切换用户态执行上下文的api,借助ucontext库函数可以实现有栈协程。
ucontex函数族主要包括如下四个api:
- void makecontext(ucontext_t* ucp, void (*func)(), int argc, ...)
- 初始化一个ucontext_t,并指明了入口函数
- int swapcontext(ucontext_t* olducp, ucontext_t* newucp)
- 保存当前上下文并切换到新的上下文运行,一般需要显示的初始化栈信息以及信号掩码集同时也需要初始化uc_link,以便程序退出上下文后继续执行
- int setcontext(const ucontext_t* ucp)
- 切换当前上下文为ucp中的上下文,执行成功不返回,直接切换到新的执行状态
- int getcontext(ucontext_t* ucp)
- 将当前执行的上下文保存在ucp中,便于之后恢复
ucontext_t结构如下:
typedef struct { ucontext_t *uc_link; sigset_t uc_sigmask; stack_t uc_stack; mcontext_t uc_mcontext; ... }
- uc_link
- 为当前context执行结束之后要执行的下一个context,若uc_link为空,执行完当前context之后退出程序。
- uc_sigmask
- 执行当前上下文过程中需要屏蔽的信号列表,即信号掩码
- uc_stack
- 为当前context运行的栈信息。
- uc_mcontext
- 保存具体的程序执行上下文,如PC值,堆栈指针以及寄存器值等信息。它的实现依赖于底层,是平台硬件相关的。此实现不透明。
优秀开源库