对协程的理解
来源:互联网 发布:蒙科立蒙古文网络平台 编辑:程序博客网 时间:2024/06/06 04:44
转载自http://blog.jqian.net/post/coroutine.html
特点
从函数的角度看:
- 协程避免了传统的函数调用栈,几乎可以无限递归。
从线程的角度看:
- 协程没有上下文切换,几乎可以无限并发;
- 协程在用户态进行显式的任务调度,可以把异步操作转换成同步操作,即意味着无须加锁。
调用栈
传统的函数,也叫子例程,是通过调用栈来传递调用关系的。协程则是比子例程更一般化的概念。
子例程的调用是LIFO,它的启动位置是唯一入口,且只能返回一次;而协程允许有多个入口,且可以返回多次(yield),你可以在特定的地方暂停和重新执行它。
上下文切换
上下文切换最早是指进程的上下文切换(context switch),它发生在内核态。内核调度器会对每个CPU上执行的进程进行调度(scheduling)),以保证每个进程都能分到CPU时间片。
当一个进程的时间片用完,或被中断后,内核将保存该进程的运行状态(即上下文),将其存入运行队列(run queue),同时让新的进程占用CPU。
进程的上下文切换包括内存地址空间、内核态堆栈和硬件上下文(CPU寄存器)的切换,所以代价很高(具体参阅UTLK进程一章)。
由于进程切换开销大,所以设计了线程。Linux 2.6内核的clone()系统调用已经支持创建内核级线程,且发布了内核线程库pthread。
在同一进程内的线程可以共享进程的地址空间,线程仅需要维护自己的寄存器、栈和线程相关的变量。
不过内核线程的调度仍然需要由内核完成,这需要进行用户态和内核态的模式切换,至少包括堆栈和内存映射的切换。
而且,不同进程之间的线程切换,有可能会还会导致进程切换,所以代价还是不小。
而协程始终运行在一个线程之内,完全没有上下文切换,因为它的上下文是维护在用户态开辟的一块内存里,而它的任务调度是在代码里显式处理的。
目前Linux上可选用的纤程库是GNU Portable Threads(Pth)。
任务调度
进程、线程和协程的设计,都是为了并发任务能够更好的利用CPU资源,他们最大的区别即在于对CPU的使用上(任务调度):
如前文所述,进程和线程的任务调度由内核控制,是抢占式的;而协程的任务调度在用户态完成,需要在代码里显式的把CPU交给其他协程,是协作式的。
由于我们可以在用户态调度协程任务,所以,我们可以把一组互相依赖的任务设计成协程。这样,当一个协程任务完成之后,可以手动进行任务调度,把自己挂起(yield),切换到另外一个协程执行。
这样,由于我们可以控制程序主动让出资源,很多情况下将不需要对资源加锁。
示例
引用一个stackless里的例子,文中是python的写法,以下是c语言版本的:
#include <stdio.h>void ping();void pong();void ping(){ printf("ping\n"); pong();}void pong(){ printf("pong\n"); ping();}int main(int argc, char *argv[]){ ping(); return 0;}
#include <ucontext.h>#include <stdio.h>#define MAX_COUNT (1<<30)static ucontext_t uc[3];static int count = 0;void ping();void pong();void ping(){ while(count < MAX_COUNT){ printf("ping %d\n", ++count); // yield to pong swapcontext(&uc[1], &uc[2]); }}void pong(){ while(count < MAX_COUNT){ printf("pong %d\n", ++count); // yield to ping swapcontext(&uc[2], &uc[1]); }}int main(int argc, char *argv[]){ char st1[8192]; char st2[8192]; // initialize context getcontext( &uc[1] ); getcontext( &uc[2] ); uc[1].uc_link = &uc[0]; uc[1].uc_stack.ss_sp = st1; uc[1].uc_stack.ss_size = sizeof st1; makecontext (&uc[1], ping, 0); uc[2].uc_link = &uc[0]; uc[2].uc_stack.ss_sp = st2; uc[2].uc_stack.ss_size = sizeof st2; makecontext (&uc[2], pong, 0); // start ping-pong swapcontext(&uc[0], &uc[1]); return 0;}
这时候,ping pong的循环调用并不依赖于调用栈,所以也就不会有调用栈溢出的风险了。而且手工调度协程,静态变量也可以无锁访问。
不过manual上说getcontext, setcontext, makecontext, swapcontext这系列函数并没有被posix接受,为了兼容性考虑,推荐使用pthread库……我想大概一般能够用coroutine解决的问题,用pthread也能解决,至多就是多加一些锁呗。
而如果要使用coroutine的话,代码编写者必须自己理清所有的调度逻辑,可能容易滋生bug,就跟setjmp和longjump似的,虽然威力强大,但一般人不推荐。
- 对协程的理解
- 对协程的理解
- 我对协程coroutine的理解
- 浅谈我对协程的理解
- 浅谈我对协程的理解
- 对Unity协程的深入理解
- 【转】浅谈对协程的理解
- 浅谈对协程的理解
- 浅谈我对协程的理解
- 对Python协程的理解
- 对Unity协程的理解
- 谈谈对协程的理解
- Unity3D中对协程和yield的理解
- 对博客的理解
- 对动力的理解
- 对类的理解
- 对package的理解
- 对IRP的理解
- hdu 5364 Distribution money
- uvalive 2191(BIT)
- [leetcode] N-Queens
- 约瑟夫环 不同密码
- CSS 进阶篇
- 对协程的理解
- JdbcTemplate的使用
- 【JCC技术】JCC功能演示
- hdu1698
- HDU 1200 To and Fro
- Java读取文件MD5的两种方案
- C++猴子吃桃问题
- 大数相加
- KRUSKAL算法