在linux系统中,默认可以通过创建进程和线程运行程序,而在内核中,被调度的实体单位是进程(task),而线程是只是创建的时候没有独立的分配地址空间。但无论如何,进程和线程是被调度的最小单元。
假设我们要设计一个极高性能的软件程序,能不能通过一些软件的技巧,尽可能少的创建线程或者进程,从而可以减少系统调度器的调度负载,同时如果一个任务不经过系统的上下文切换,自然其整体性能是高于线程和进程的。
在C语言中,可以通过自己实现协程来做到这一点,协程在操作系统层面上是看不到的,它只能看到进程和线程,但是在代码层面,他是“多任务并发”的。本文简单介绍一下协程
开源软件putty就使用了无栈协程,它是基于goto的特性实现,switch case就是一种goto的体现。详细可以如下了解
https://www.chiark.greenend.org.uk/~sgtatham/coroutines.html
以一个例子说明
#define crBegin static int state=0; switch(state) { case 0: #define crReturn(i,x) do { state=i; return x; case i:; } while (0) #define crFinish } int main(void) { static int i; crBegin; for (i = 0; i < 10; i++) crReturn(1, i); crFinish; }
此函数通过宏展开其实就是简单的0,9的循环体,如下
# gcc test.c -E -o test.E int main(void) { static int i; static int state=0; switch(state) { case 0:; for (i = 0; i < 10; i++) do { state=1; return i; case 1:; } while (0); }; }
接下来事情就简单了,如果实现一个yield函数,此函数在switch里面如果能够动态的修改case的值,那么它就好像是多线程一样运行,yield的实现如下
#define cr_yield \ { \ __s = __LINE__; \ usleep(THREAD_INTERVAL); \ return; \ case __LINE__:; \ }
那么这句yield的语义就是将当前任务让出,由其他协程来继续运行。同操作系统调度的yield语义一致。
下面提供一个完整的无栈协程的例子
#include <stdio.h> #include <unistd.h> #define THREAD_INTERVAL 1000 * 500 #define cr_start() \ static int __s = 0; \ switch (__s) { \ case 0: #define cr_yield \ { \ __s = __LINE__; \ usleep(THREAD_INTERVAL); \ return; \ case __LINE__:; \ } #define cr_end() \ } \ __s = 0; static int condition = 1; static void coroutine_1() { cr_start(); for (;;) { /* do something */ printf("Run %s\n", __FUNCTION__); cr_yield; } cr_end(); } static void coroutine_2() { cr_start(); for (;;) { if (condition) { /* do something conditional */ printf("Run %s - Action(1)\n", __FUNCTION__); cr_yield; } /* do something */ printf("Run %s - Action(2)\n", __FUNCTION__); condition = !condition; cr_yield; } cr_end(); } int main() { for (;;) { coroutine_1(); coroutine_2(); } return 0; }
编译并运行,日志如下
Run coroutine_1 Run coroutine_2 - Action(1) Run coroutine_1 Run coroutine_2 - Action(2) Run coroutine_1 Run coroutine_2 - Action(2) Run coroutine_1 Run coroutine_2 - Action(1) Run coroutine_1 Run coroutine_2 - Action(2) Run coroutine_1 Run coroutine_2 - Action(2)
可以看到,通过switch和case以及__LINE的动态更新,让代码好像是两个线程在运行一样。但实际上,此段代码是在一个线程内顺序执行的。
论文 Protothreads 实现了更成熟的无栈协程。下面基于通过例子演示一下基于Protothreads的co-routine
首先需要Protothreads实现的头文件,如下
提供lc-switch.h文件
#ifndef LC_SWITCH_H_ #define LC_SWITCH_H_ /** \hideinitializer */ typedef unsigned short lc_t; #define LC_INIT(s) s = 0; #define LC_RESUME(s) switch(s) { case 0: #define LC_SET(s) s = __LINE__; case __LINE__: #define LC_END(s) } #endif /* LC_SWITCH_H_ */ /** @} */
提供lc.h文件
#ifndef LC_H_ #define LC_H_ #ifdef LC_CONF_INCLUDE #include LC_CONF_INCLUDE #else /* LC_CONF_INCLUDE */ #include "lc-switch.h" #endif /* LC_CONF_INCLUDE */ #endif /* LC_H_ */ /** @} */ /** @} */
提供pt.h文件
#ifndef PT_H_ #define PT_H_ #include "lc.h" struct pt { lc_t lc; }; #define PT_WAITING 0 #define PT_YIELDED 1 #define PT_EXITED 2 #define PT_ENDED 3 #define PT_INIT(pt) LC_INIT((pt)->lc) #define PT_THREAD(name_args) char name_args #define PT_BEGIN(pt) { char PT_YIELD_FLAG = 1; if (PT_YIELD_FLAG) {;} LC_RESUME((pt)->lc) #define PT_END(pt) LC_END((pt)->lc); PT_YIELD_FLAG = 0; \ PT_INIT(pt); return PT_ENDED; } #define PT_WAIT_UNTIL(pt, condition) \ do { \ LC_SET((pt)->lc); \ if(!(condition)) { \ return PT_WAITING; \ } \ } while(0) #define PT_WAIT_WHILE(pt, cond) PT_WAIT_UNTIL((pt), !(cond)) #define PT_WAIT_THREAD(pt, thread) PT_WAIT_WHILE((pt), PT_SCHEDULE(thread)) #define PT_SPAWN(pt, child, thread) \ do { \ PT_INIT((child)); \ PT_WAIT_THREAD((pt), (thread)); \ } while(0) #define PT_RESTART(pt) \ do { \ PT_INIT(pt); \ return PT_WAITING; \ } while(0) #define PT_EXIT(pt) \ do { \ PT_INIT(pt); \ return PT_EXITED; \ } while(0) /** @} */ #define PT_SCHEDULE(f) ((f) < PT_EXITED) /** @} */ #define PT_YIELD(pt) \ do { \ PT_YIELD_FLAG = 0; \ LC_SET((pt)->lc); \ if(PT_YIELD_FLAG == 0) { \ return PT_YIELDED; \ } \ } while(0) #define PT_YIELD_UNTIL(pt, cond) \ do { \ PT_YIELD_FLAG = 0; \ LC_SET((pt)->lc); \ if((PT_YIELD_FLAG == 0) || !(cond)) { \ return PT_YIELDED; \ } \ } while(0) /** @} */ #endif /* PT_H_ */ /** * @} * @} */
上面是全部Protothreads的实现了,下面编写测试程序
#include <stdio.h> #include "pt.h" static struct pt co_pt; static int coroutine(struct pt *pt) { PT_BEGIN(pt); printf("[%s]: Start\n", __func__); PT_YIELD(pt); printf("[%s]: After yield\n", __func__); PT_END(pt); } int main() { PT_INIT(&co_pt); coroutine(&co_pt); coroutine(&co_pt); return 0; }
编译运行日志如下
tf@tf:# ./test [coroutine]: Start [coroutine]: After yield
到这里,无栈协程完全说清楚了。
无栈协程的特点是简单,但是正因为没有栈的实现,所以局部变量无法保持。同时,有栈协程的体验会更像线程,对于无栈协程,错误的开发可能导致内存泄漏无法察觉。而开辟栈区的协程,可以清晰的管理这些事情。
开源的有栈协程库为 neco 。下面基于此简单说明一下
git clone https://github.com/tidwall/neco.git
根据仓库的README.md的指示,测试程序如下
#include <stdio.h> #include "neco.h" void ticker(int argc, void *argv[]) { while (1) { neco_sleep(NECO_SECOND); printf("tick\n"); } } void tocker(int argc, void *argv[]) { while (1) { neco_sleep(NECO_SECOND*2); printf("tock\n"); } } int neco_main(int argc, char *argv[]) { neco_start(ticker, 0); neco_start(tocker, 0); // Keep the program alive for an hour. neco_sleep(NECO_HOUR); return 0; }
编译运行后,日志如下
tf@tf:# ./test tick tock tick tick tock tick tick tock tick
本文简单介绍了C语言的协程,协程对于高性能应用开发来说必不可少。