1 协程的运用场景
在网络高并发的场景下,epoll作为解决方案的首选,基本可以达到C1000K甚至更好的并发量,但是由于需要依靠线程进行异步的回调操作,在一定意义上资源的开销并没有降低,另一方面异步操作带来的问题就是整体复杂度大大提高。协程就是为了解决上述的问题,同步的编程方式,达到异步的性能,最重要的是资源的开销更是小于多线程。
3 协程原理
协程包括几大部分组成,一个是协程本身,第二个是协程的调度器,第三是协程的切换。整体过程就是当协程创建完成之后,将协程加入到调度器统一进行管理,执行协程是由统一的调度算法来决定,协程具体操作是遇到IO操作之前调用yeild让出执行权,然后由调度器决定下一个换入resume操作,加载下一个执行的协程,执行完成后将回到yeild位置继续执行IO操作。整体给人的感觉就是整个调度器将时间划分为不同的时间片,并且交由不同协程去完成相关的操作。没有操作时,主动权将移交给调度器统一控制。
3.1 协程的切换
上面可以看到协程两个重要的原语操作,一个是yeild主要负责将当前的操作保存,切换到上一次resume位置继续执行,另一个是resume,将当前的操作保存,回到上一次yeild操作。两个原语操作是相互可逆的操作,实现了从调度器切换到协程A运行,然后由协程A切换回调度器,执行协程B,协程B的执行完成,再次回到协程A继续执行,如此反复,保证调度器有任务处理的时候始终处于任务处理状态,而不会出现等待的情况。
如何实现这一技术,主要是从CPU工作的原理出发,当程序执行时,主要是利用寄存器进行数据计算,然后利用栈进行数据的临时数据的保存。为了做到协程A和协程B之间的相互隔离,需要保护的现场包括,运算寄存器的值和,堆栈的信息,两部分组成。
3.2 协程的定义
协程的定义涉及到多方面的定义,一个是上下文的定义,上下文主要是包括寄存器状态和堆栈的信息状态。
//寄存器,栈信息
typedef struct _haw_reg_ctx {
void *reg[16];//寄存器信息
void *stack; //栈指针
size_t stack_size;//栈大小
}haw_reg_ctx;
第二部分是一个协程的定义,协程主要包括几大部分,首先是协程自身的一个ID用于标识协程;第二个是协程的状态,目前初步规划是就绪状态、等待状态、睡眠状态和结束状态四种;第三协程需要一个上下文,用于保存协程的运行时状态主要是方便恢复;第四部分是协程程序体,包括协程的参数;第五部分是协程的调度器,保存调度器的指针;第六部分是预留的节点,用于各个状态之间的切换用。
这里引出了一个问题就是就绪队列,睡眠队列,等待队列到底用什么数据结构保存。首先是就绪队列,这里采用的是队列的方式,主要是考虑到协程的运行并没有优先级,采用先进先出的方式,保证协程可以被很好的执行到。至于就绪队列和等待队列考虑到用红黑树,主要是为了取数据方便,根据睡眠或者等待的时间进行中序遍历,可以方便的取出其中小于某个时间的多个协程节点。
typedef enum _haw_co_status {
HAW_CO_READY,
HAW_CO_SLEEP,
HAW_CO_WAIT,
HAW_CO_DEAD,
}haw_co_status;
//协程
typedef struct _haw_coroutine {
uint64_t id; //协程ID
uint64_t time; //创建时间
haw_co_status status; //协程状态
haw_reg_ctx *ctx; //协程的上下文
void *func;//协程执行体
void *arg;//传递参数
haw_co_schedule *sch;//调度器
queue(haw_coroutine) ready_node;//就绪队列节点
rbtree(haw_coroutine) sleep_node;//睡眠红黑树节点
rbtree(haw_coroutine) wait_node; //等待红黑树节点
}haw_coroutine ;
3.3 协程的调度器
调度器整体分为三个部分,一个是当前协程的一些信息,另一个是epoll事件管理;第三部分是协程队列用于保存所有协程的信息。
//调度器
typedef struct _haw_co_schedule {
//当前协程信息
uint64_t time;
haw_reg_ctx *ctx;
haw_coroutine *cur_co;
//epoll io事件管理
int epfd;
int socketfd;
struct epoll_event *events;
int events;
haw_co_queue ready;//就绪队列
haw_co_rbtree sleep;//睡眠红黑树
haw_co_rbtree wait;//等待红黑树
}haw_co_schedule ;
3.4 协程的调度策略
调度策略这里采用的是平行的调度策略,谁有时间就行相关处理,对于睡眠队列,当超时时间到后即进行恢复运行权;等待队列是当接收到epoll触发事件后,进行运行权利的恢复;对于就行队列,只有存在元素即可恢复程序的运行权。
//核心调度逻辑
while(1)
{
//遍历所有睡眠集合,使用resume恢复到期的协程运行权
haw_coroutine *exp_co = NULL;
while((exp_co = sleep_rbtree_expired(schedule)) != NULL)
{
resume(exp_co);
}
//遍历所有等待集合,使用resume恢复等待协程的运行权
haw_coroutine *wait_co = NULL;
int nready = epoll_wait(schedule->epfd, events, MAX_EPOLL, 1);
for(i = 0; i < nready; i++)
{
wait_co = wait_rbtree_search(events[i].data.fd);
resume(wait_co);
}
//遍历所有的就绪队列,使用resume恢复就绪协程的运行权
haw_coroutine *ready_co = NULL;
while(!queue_empty(schedule->ready))
{
ready_co = queue_pop(schedule->ready);
resume(ready_co);
}
}