总所周知,协程这个概念已是服务端开发领域中耳熟能详的名词了。说协程是一组程序组件,以往的多线程编程有个特色是须要来回进行系统级别的来回上下文切换,形成很大的系统开销,不只如此,不少操做咱们还须要保证原子性,加锁,锁这个东西嘛,原本就是个坑,能不能最好仍是不要用了。协程就是这么牛,能解决上述出现的全部问题,由于协程是用户态轻量级的多线程,上下文切换的开销是很是小的,并且更重要的是,是用户主动去进行切换的,所以不存在这个操做执行到一半,就被另外一个线程给打断了。那么协程常见的用例都有哪些呢?固然能够用来作状态机,可读性很高。也能够用来作角色模型和产生器。大牛们也在不断在造轮子,好比有很是牛逼的云风大叔也造了一个简易的协程框架,代码很是精简,很是适合学习,真心点个赞!后面咱们会着重分析云风大叔的代码。git
下面咱们来介绍几个相关的 C 库函数:setcontext
、getcontext
、makecontext
和 swapcontext
是用来作 context
控制的。setcontext
能够被看作是一个 setjmp
/longjmp
的高级版本。github
在 ucontext.h 这个系统的头文件上定义了 ucontext 的结构体,咱们能够看到结构体以下所示:编程
typedef struct ucontext { struct ucontext *uc_link; sigset_t uc_sigmask; stack_t uc_stack; mcontext_t uc_mcontext; ... } ucontext_t;
这是最重要的结构体,让咱们来分析一下这个这个结构体。
若是上下文被用 makecontext
来建立时,uc_link
指向的是当前上下文退出时候将会被 resumed
的上下文。uc_sigmask
被用来存储在上下文中一组被阻塞的信号, uc_stack
是一个被上下文使用的 stack
,uc_mcontext
用来存储执行状态,包括全部的寄存器和 CPU flags
、指令指针和栈指针。数组
int setcontext(const ucontext_t *ucp)
这个函数会把当前上下文转移到上下文 ucp
中。该函数不会返回,从 ucp 这个指针中执行。数据结构
int getcontext(ucontext_t *ucp)
该函数会保存当前的上下文信息到 ucp 中。多线程
void makecontext(ucontext_t *ucp, void *func(), int argc, ...)
在被以前使用 getcontext
初始化后的 ucp
中设置一个替代的控制线程, ucp.uc_stack
成员应该被指向合适大小的栈,常量 SIGSTKSZ
一般会被使用。当使用 setcontext
或 swapcontext
跳转的时候,执行将从 func
指向的函数的入口点开始,固然别忘了指定 argc
参数,表示参数个数。当 func
终止的时候,控制权被返回到 ucp.uc_link
。框架
int swapcontext(ucontext_t *oucp, ucontext_t *ucp)
转到 ucp
上下文中执行并保存当前上下文到 oucp
中函数
下面咱们来看一个简单的示例:学习
#include <stdio.h> #include <ucontext.h> #include <unistd.h> int main(int argc, const char *argv[]){ ucontext_t context; getcontext(&context); puts("Hello world"); sleep(1); setcontext(&context); return 0; }
结果的输出是:
Hello world
Hello world
Hello world
Hello world
...线程
是否是感受这个世界很奇妙!
如今请把目光转移到 c 协程
主要定义了几个数据结构和函数,如今来分析一下如何实现的。
先建立一个结构体
struct schedule { char stack[STACK_SIZE]; // 运行的协程的栈 ucontext_t main; // 下个要切换的协程的上下文状态 int nco; // 当前协程的数目 int cap; // 协程总容量 int running; // 当前运行的协程 struct coroutine **co; // 协程数组,指向指针的指针 co };
struct coroutine { coroutine_func func; // 调用函数 void *ud; // 用户数据 ucontext_t ctx; // 保存的协程上下文状态 struct schedule * sch; // 保存struct schedule指针 ptrdiff_t cap; // 上下文切换时保存的栈的容量 ptrdiff_t size; // 上下文切换时保存的栈的大小 size <= cap int status; // 协程状态 char *stack; // 保存的栈 };
先调用 coroutine_open
来建立一个 schedule
结构体
struct schedule * coroutine_open(void) { struct schedule *S = malloc(sizeof(*S)); // S 是指针,*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); return S; }
后面调用 coroutine_new
来建立协程,若是当前的协程数目小于容量,直接加进去,不然,扩容为当前的2倍,并返回 id
。
后面就能够开始 resume
了,内部的实现细节是,先看看要执行的协程的状态是什么,若是是 ready
的话,那就先获取当前的上下文信息到协程的ctc中,设置栈,设置改协程终止时下一个要执行的协程,此处为 &S->main
。设置状态为正在执行。设置该上下文指向的函数,此处为 mainfunc
,利用 swapcontext
去执行上下文 &C->ctx
并保存当前的上下文信息到 &S->main
。
总的来讲,云风大叔写的代码十分通俗易懂,若有不明白的地方请留言,我将会尽快帮助您解答。