拥抱新技术 —— 协程
写在前面:
本文主要针对使用C/C++或有C/C++基础的读者,讨论的开源网络库libco
什么是协程
很多人描述协程为 “轻量级线程” 、 “用户态线程” 等等。之前博主对这个东西也很迷惑?
这都是啥!!!不急,博主将会写一系列文章来理清楚协程,此为系列文章的第一篇,主要介绍协程的一些基本知识:
为了理清楚协程,我们还得对线程的概念进行回忆一下:
:
线程是操作系统能够进行运算调度的最小单位。被包含在进程之中,是进程的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程可以并发执行多个线程,每个线程会执行不同的任务。对应在现实生活中,进程是组长,线程是小组成员。
既然说线程是系统调度的最小单位,那么又何来说协程的概念呢?通过阅读大量的文献以及之后,我对协程有了自己的看法。
下面我将一条一条来解读这些特性:
协程之于线程
刨除操作系统CPU对线程调度的因素,我们就单考虑一个线程之中的情况。
首先,协程之于线程跟线程之于进程是一样的,一个线程之中是有多个协程的。
可以把线程想象成一个单核CPU,而协程A、B都是原来的进程。协程A、B通过一个叫协程调度器的东西来负责协程A、B的交替执行。
注:
以上情况只涉及libco 中的设计,go语言中的协程其可以运行在多个“CPU”(线程)。
在libco中,一个协程只能运行在一个线程中,这也就是说它为什么在线程中是串行的原因。因为对于一个单核CPU中,也没法进行两个进程的并行啊。
协程的切换
切成的切换得益于其在用户态就保存了栈和寄存器信息。这里面有当前协程的状态信息,有下一条执行的地址信息。这么说有点抽象,我们看一下下面的一个例子吧:
假设协程A正在执行以下函数:
void funa(void)
{int a = 10;int b = 20;sum(a,b);
}
如果说当协程A执行到sum函数的时候,协程调度器调度协程B,协程A的信息就会被保存起来。那么当前协程的的运行状态信息就是 a=10,b=20,下一条该执行的函数是sum。等待下次协程A被调度器调度,这些信息就会被恢复,再次从协程A去执行。
非对称协程和对称协程
协程目前分两种,一种是go语言采用的对称协程;一种是libco采用的非对称协程。
这里借用知乎博主tx征服者的图来向大家说明:
对称协程其实就是由协程调度器来负责,协程不允许调度其他协程。调度器协程A,那么协程A会yeild回调度器,再由调度器去执行其他的协程。如果我们把的虚线都放在左边,yeild的实线都放在右边。以调度器为中心,那么他是不是就是一个对称的图形呢?
我们再来看一下非对称协程:
非对称调度由调度器来调度协程A,然后协程A再调动协程B。那么让出CPU使用权就不是让给调度器了,而是协程A。简而言之就是从哪儿来,回哪儿去。
同样,将此展开也不是一个对称的图形了。
独占栈和共享栈
独占栈( )即为每个协程分配一个单独的、固定大小的栈;
共享栈( the stack)则仅为当前正在运行的协程分配栈内存;
共享栈这里其实就是当协程被切出去时,就把它实际占用的栈内存copy保存到一个单独分配的缓冲区;当被切出去的协程再次调度执行时,再一次copy将原来保存的栈内存恢复到那个共享的、固定大小的栈空间。
libco将两者都实现了,默认使用前者。
定时器
为什么要介绍定时器呢?是因为定时器是协程调度中不可缺失的一个部分。就跟CPU的时间片轮转法一样,协程调度器通过定时器来调度协程。
定时器是事件驱动模型的网络框架的必不可少的功能。网络I/O的超时、定时任务,包括定时等待都依赖于此。
一般而言,使用定时功能时,我们首先向定时器中注册一个定时事件,在注册定时事件时需要指定这个事件在未来的触发事件。在到了触发时间点后,我们会收到定时器的通知。
网络中的定时器,一般可以看做两部分组成:
libco中则是直接使用epoll来定时,其中的定时器实现算法如下:
:调用等待I/O就绪事件,最大等待时长设置为1毫秒处理I/O就绪事件:循环处理得到的I/O就绪文件描述符从时间轮取超时事件:从时间轮取超时事件,放到队列处理超时事件:如果取到的超时事件不为空,那么就循环处理队列中的定时任务跳到步骤1 参考文献
[1] C++开源协程库libco详解
[2]协程学习(对称和非对称).tx征服者.知乎