从事C++多年,没有留下什么。最近突然想写一些东西,为自己的程序员生涯留下一些痕迹。想到哪儿写到哪儿吧。
具体的代码可参考 taro-dady/co_taro: coroutine framework (github.com)
一 、什么是协程
以我个人的粗浅理解,协程就是把线程的时间片进行拆分。本来一个线程在操作系统的分配下得到的时间片,又被协程拆分了。线程的调度要靠内核,而协程的调度只需要在用户态就可以了。
二、协程使用场景
任务的切换效率:
不同进程间的线程切换 < 同进程的线程切换 < 同线程的协程切换
任务分类:
a. 计算密集型
计算密集型的任务需要长时间使用CPU,因此并不太适合使用协程,可能一个线程时间片都不够计算使用就没有必要再关注切换的问题了
b. IO密集型
IO密集型的任务CPU使用的时间短,等待IO就绪的时间长,这种情况下使用协程是一个较好的选择
三、协程的核心概念
协程的核心概念有两个: 第一是协程切换方法,第二是协程的调度策略
3.1 协程切换
Linux | Windows |
ucontext(C) | fiber(C) |
setjmp/longjmp | setjmp/longjmp |
寄存器(汇编) | 寄存器(汇编) |
具体的分析就不多做赘述了,网上分析的文章很多,各位自行查阅即可。我在此选择了汇编操作寄存器的方案。
协程的栈:用来保存被切换出去的协程的寄存器的值,当该协程被调度时将栈里的数据还原到寄存器中
有栈协程 | 静态栈:每个协程单独的固定大小栈。好处是比较独立,坏处是大小不好分配 |
共享栈:多个协程共享一个栈,协程自身的栈数据保存在自己申请的内存中,需要切换时把自己内存中的数据拷贝到共享栈中,完成协程切换 | |
虚拟栈:进程申请的内存并不会 比如: 用malloc申请一个内存比如2M,申请完成后并没有在物理上实际使用2M,只有在使用时才会进行实际的分配内存。内存分配是按页分配的(4K),也就是最多也就冗余4K - 1的内存 | |
无栈协程 | 无栈协程有点像状态机,全程只用了一个栈,把一个协程拆分成很多小块,这些小块的入口只有一个,然后通过改变state的值来实现协程的不同逻辑。这种实现无需保存协程的栈帧,可以节省内存提高速度,但是不能实现协程的任意切换(C++20用的是无栈协程) int state = 0; if ( 0 == state ){ func1(); }else if( 2 == state ){ func2(); } void func1(){ state = 1; ....} void func2(){ state = 0; .... }; |
3.2 协程调度
3.2.1 非对称协程
非对称协程指的是协程之间有绑定调用关系,从一个协程切换到新协程后,新协程就只能返回到原调用方协程。
3.2.2 对称协程
每个协程的地位关系都是平等的,可以由任意一个协程切换到任意另外一个协程。
3.2.3 协程调度器
a. 不跨线程的协程调度
协程只会被一个线程调度,这样的好处是可以不考虑线程并发的问题,用同步的方式实现异步编程模型。坏处是协程的分配不均匀,导致某些线程负载很高,而某些负载很低的情况发生
b. 跨线程的协程调度
协程会被多个线程调度,好处是可以平衡线程负载,坏处是协程需要考虑线程并发竞争问题
c. 混合协程调度
每个CPU都有自己的协程队列,当本地的协程队列没有协程时,从全局协程队列中取协程到本地队列