文章目录
一、简介
- 对于线程,其切换是由
操作系统
根据其系统内核中的调度器
,抢占式
地进行的,不能很好地保证线程间的先后顺序
;- 协程可以理解为一种用户态的
轻量级线程
,切换由用户定义
,各任务之间可以控制执行
、暂停
、恢复函数
,来达到多任务协作的目的;- 协程上下文切换速度快, 且不会陷入
内核态
;- 协程拥有独立的
寄存器上下文和栈
,协程调度切换时,将寄存器上下文和栈,在来回切换时,恢复先前
保存的寄存器上下文和栈;
优点
- 协程具有
极高的执行效率
,由于子程序切换
不是线程切换,是由程序自身控制
,故协程没有线程切换
的开销- 多线程的
线程数多
,协程的性能越显著
;- 访问
共享资源
不需要使用多线程的锁机制
和变量冲突
,由于只有一个线程
,故在协程只需要判断状态
即可,降低了编码难度;- 以
同步代码
的方式写异步
逻辑;
对称与非对称
【非对称】:是跟一个特定的调用者绑定的,协程让出CPU时,只能让回给原调用者;
协程间的地位不对等,只能返回最初调用它的协程;
【如】:ucontext、libco
【对称】:启动之后和之前的协程没有任何关系;
参考:https://zhuanlan.zhihu.com/p/363775637
对称协程,进入到该协程之后只能有一个操作就是yield,把cpu让回给调度器;
非对称协议,可以有两个操作,就是resume和yield,从哪里resume的,yield就会回到该位子;
1.1 常见协程库
1.1.1 boost.context
提供了上下文的抽象,并给了两种方式,fiber和call/cc的方式保留和执行上下文切换;
- 性能佳,推荐使用,切换性能可达到1.25亿次/秒;
1.1.2 boost.coroutine
提供的协程只能
单向传递
数据,数据只能单向的从一个代码块流向另一个代码块。流入流出分别对应着push_type
和pull_type
类型,由这两个类型组成协程间跳转的通道,同时也是数据传递的通道;
1.1.3 ucontext
该库是在
unix
下提供的,使用是最安全可靠
,但性能较差
,大概200万次/秒;
1.1.4 fiber
该库是在
window
下提供的,与ucontext类似;
1.1.5 libgo
libgo为了有更广阔的适用性,支持了多线程调度、HookSyscall、Worksteal等,同时突破了
传统协程库
仅用来处理网络io密集型
业务的局限,也能适用于cpu密集型业务
,充当并行编程库
来使用;
1.2 协程栈
1.2.1 静态栈
固定大小的栈,容易造成
溢出
等现象;
1.2.2 分段栈
插入栈内存
检测代码
,若栈不够用,则申请新内存扩展
;但该方法难以在第三方库中进行使用;
1.2.3 共享栈
- 申请一块大内存作为
共享栈
,在运行前,先把协程栈的内存
copy到共享栈中,运行结束后再计算协程栈真正使用的内存,copy出来保存起来,这样每次只需保存真正使用到的栈内存量
即可;- 该方案极大程度上
避免了内存的浪费
,做到了用多少占多少,同等内存条件下,可以启动的协程数量更多;- 但该方案在copy上花费了时间,
降低速度
,导致协程切换慢
;
1.2.4 虚拟内存栈
- 机制:进程申请的内存并不会
立即
被映射成物理内存
,而是仅管理于虚拟内存中,真正对其读写时会触发缺页中断
,此时才会映射为物理内存;- 可以做到用多少占多少,冗余不超过一个内存页大小;
1.3 协程调度
1.3.1 栈式调度
协程队列是一个
栈式
结构,创建的协程都置于栈顶
,并且会立即暂停当前协程并切换至子协程中运行,子协程运行结束后,继续切换回来执行父协程;越是栈底部的协程,被调度到的机会将越少,甚至出现只有栈顶的协程在互相切换;
1.3.2 星切调度
调度线程 -> 协程A -> 调度线程 -> 协程B -> 调度线程 -> …
;- 将当前可调度的协程组织成
队列
,按顺序从头部
取出协程调度;新协程则从尾部入队,调度后再将协程从尾部入队;
.3.3 环切调度
调度线程 -> 协程A -> 协程B -> 协程C -> 协程D -> 调度线程 -> …
- 从调度顺序上可知,环切的
切换次数
仅为星切的一半
,可以提高整体切换速度
;但在多线程调度、WorkSteal方面会带来一定的挑战;
二、Linux下ucontext
库
2.1 getcontext
/**
* int getcontext(ucontext_t *ucp);
* -------
* func:函数将ucp指向的结构初始化为调用线程的当前用户上下文;
* param ucp:用户上下文,包括调用线程的机器寄存器的内容、信号掩码和当前执行堆栈;
* return:成功不返回, 否则返回-1;
* */
2.2 setcontext
/**
* int setcontext(const ucontext_t *ucp);
* -------
* func:函数恢复ucp指向的用户上下文;
* param ucp:用户上下文,包括调用线程的机器寄存器的内容、信号掩码和当前执行堆栈;
* return:成功返回0, 否则返回-1;
* */
2.3 makecontext
/**
* void makecontext(ucontext_t *ucp, (void *func)(), int argc, ...);
* -------
* func:修改ucp指定的上下文;
* param ucp:调用的函数,在调用 makecontext()之前,被修改的上下文应该有一个为其分配的堆栈;
m_ctx.uc_link = nullptr; // 当前上下文返回之后指向的上下文
m_ctx.uc_stack.ss_sp; // 栈空间
m_ctx.uc_stack.ss_size ; // 栈大小
* param argc:argc的值 必须与传递给func的整数参数的数量相匹配 ,否则行为未定义;
* */
2.4 swapcontext
/**
* int swapcontext(ucontext_t *oucp, const ucontext_t *ucp);
* -------
* func:当前上下文保存在 oucp指向的上下文结构中,并将上下文设置为ucp 指向的上下文结构;
* param oucp:上下文;
* param ucp:上下文;
* return:成功返回0, 否则返回-1;
* */