协程二三事(1)

1. 协程介绍

协程(coroutine)是近些年来在后台开发方向比较火的一个概念,实际上,协程在历史上比线程还要早些,而最近火起来则是因为近来后台服务开发中遇到的C10K问题导致。Wiki中给出的定义:

协程是一种程序组件,是由子例程(过程、函数、例程、方法、子程序)的概念泛化而来的,子例程只有一个入口点且只返回一次,而协程允许多个入口点,可以在指定位置挂起和恢复执行。

单看定义,比较晦涩难懂。本质上,协程可以粗略理解为用户态线程,主要是用来解决在IO Bound型服务当中,如何更加有效地榨取CPU的使用效率。(同时,还要兼顾程序开发的效率和可读性,因此异步回调的开发模式虽然在Nginx引领下大放异彩,但对程序开发人员来说并不友好)。用户态线程有两个问题必须被处理:

  • 碰到阻塞条件(或者其他触发条件),进程必须能够被挂起;
  • 由于没有时钟阻塞,进程需要有自己进行线程调度的能力。
    因此,如果一种“用户态线程”的实现,使得每个线程可以通过自己调用某个方法(如yield等),主动交出控制权,那么我们就称这种用户态线程为协程(协作式线程)。
    一个例子就是生产者消费者模型,如果在此处将线程换为协程,即一个协程负责生产产品并放入队列,另一个负责把产品从队列中取出并进行消费,同时为了提高效率可以一次生产或消费多个产品,则伪代码如下:
# producer coroutine
loop
while queue is not full
  create some new items
  add the items to queue
yield to consumer

# consumer coroutine
loop
while queue is not empty
  remove some items from queue
  use the items
yield to producer

2. 协程实现学习笔记

在C/C++中,函数调用通过压栈的方式完成,具体过程如下:

  1. 第一个进栈的是主函数中函数调用后的下一条指令(函数调用语句的下一条可执行语句)的地址;
  2. 然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变量。注意静态变量是不入栈的;
  3. 当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主函数中的下一条指令,程序由该点继续运行。

在被调函数执行完成前,主调函数无法获取其上下文,因为函数变量已经压栈暂时无法使用。因此,实现协程本质就是如何让C++实现yield的功能。

利用Linux提供的上下文保存机制,协程实现有ucontext系列和setjmp/longjmp系列。这里先对ucontext系列进行简单说明。ucontext是GNU C提供的一组用于创建、保存、切换用户态执行上下文(context)的API,主要包括以下四个函数:

void makecontext (ucontext_t *ucp, void (*func)(), int argc, ...);
int  swapcontext (ucontext_t *oucp, ucontext_t *ucp);
int  getcontext  (ucontext_t *ucp);
int  setcontext  (const ucontext_t *ucp);

typedef struct ucontext {
  struct ucontext *uc_link;
    sigset_t       uc_sigmask;
    stack_t         uc_stack;
    mcontext_t uc_mcontext;
    ...
} ucontext_t;

其中,

  1. sigset_t和stack_t定义在标准头文件中,uc_link字段保存当前context执行结束后执行的下一个context记录;
  2. uc_sigmask记录该context运行阶段需要屏蔽的信号;
  3. uc_stack是该context运行时的栈信息,最后一个字段uc_mcontext保存具体的程序执行上下文——PC值、堆栈指针、寄存器值等,实现方式与平台、硬件相关。

简单介绍下四个函数:

int makecontext(ucontext_t ucp, void (func)(), int argc, ...)

makecontext函数用来初始化一个ucontext_t类型结构,func指明该context的入口函数,argc指明参数个数,后续紧跟各个参数(每个参数都是int型);
另外,在调用makecontext之前,一般还需要显式的指明其初始栈信息(栈指针SP及栈大小)和运行时的信号屏蔽掩码(signal mask)。同时也可以指定uc_link字段,这样在func函数返回后,就会切换到uc_link指向的context继续执行。

int getcontext(ucontext_t *ucp)

getcontext用来将当前执行状态上下文保存到ucp指向的上下文结构当中,若后续调用setcontext或者swapcontext恢复该上下文,则程序会沿着getcontext调用点之后继续执行,看起来好像刚从getcontext函数返回一样。这个功能和setjmp类似,都是保存执行状态以便后续能够继续执行,但是:getcontext函数的返回值只能表示本次操作是否执行正确,而不能用来区分是直接从getcontext操作返回,还是由于setcontext/swapcontext恢复状态导致的返回,这与setjmp是不一样的。

int setcontext(const ucontext_t *ucp)

setcontext用来将当前程序执行切换到ucp所指向的上下文状态,在执行正确的情况下,setcontext直接切入到新的执行状态,不会再返回。比如我们用上面介绍的makecontext初始化了一个新的上下文,并将入口指向某函数func(),那么setcontext成功后就会马上运行func()函数。

int swapcontext(ucontext_t *oucp, ucontext_t *ucp)

swapcontext可以理解为以上两个操作的组合:

  • 首先getcontext(oucp); //保存当前上下文到oucp
  • 然后setcontext(ucp); //执行ucp上下文

理论上有上面3个函数可以满足需要。但由于getcontext不能区分返回状态,因此进行上下文切换时需要保存额外的信息来判断,比较麻烦。为了简化实现,swapcontext用来“原子”地完成旧状态的保存和切换到新状态。(并非真正的原子操作,在多线程情况下也会引入一些调度方面的问题)

一个具体的例子:

#include 
#include 

void func1(void* arg)
{
    puts("func1");
}

void func2(void* arg)
{
    puts("func2");
}

int main()
{
    char stack[1024*128];
    ucontext_t child,main;

    getcontext(&child); //获取当前上下文
    child.uc_stack.ss_sp = stack;//指定栈空间
    child.uc_stack.ss_size = sizeof(stack);//指定栈空间大小
    child.uc_stack.ss_flags = 0;
    child.uc_link = &main;//设置后继上下文

    makecontext(&child,(void (*)(void))func1,0);//设置child协程执行func1函数
    swapcontext(&main,&child);//保存当前上下文到main,执行child上下文,因为child上下文后继是main,所以执行了func1函数后,会回到此处
    puts("back to main 1st");//如果设置了后继上下文,func1函数指向完后会返回此处

    makecontext(&child,(void (*)(void))func2,0);
    swapcontext(&main,&child);
    puts("back to main 2nd");
    return 0;
}

//程序输出:
//func1
//back to main 1st
//func2
//back to main 2nd

在云风实现的协程库中,就是依据ucontext库,抽象并实现了自己的协程调度Scheduler,在Scheduler中申请了单独的栈空间,与进程执行流的栈空间进行交换保存保存,具体可以参考这一段(来自云风的协程库):

void  coroutine_resume(struct schedule * S, int id) {
    assert(S->running == -1);
    assert(id >=0 && id < S->cap);
    struct coroutine *C = S->co[id];
    if (C == NULL)
        return;
    int status = C->status;
    switch(status) {
    case COROUTINE_READY:
        getcontext(&C->ctx);
        C->ctx.uc_stack.ss_sp = S->stack;
        C->ctx.uc_stack.ss_size = STACK_SIZE;
        C->ctx.uc_link = &S->main;
        S->running = id;
        C->status = COROUTINE_RUNNING;
        uintptr_t ptr = (uintptr_t)S;
        makecontext(&C->ctx, (void (*)(void)) mainfunc, 2, (uint32_t)ptr, (uint32_t)(ptr>>32));
        swapcontext(&S->main, &C->ctx);
        break;
    case COROUTINE_SUSPEND:
        memcpy(S->stack + STACK_SIZE - C->size, C->stack, C->size);
        S->running = id;
        C->status = COROUTINE_RUNNING;
        swapcontext(&S->main, &C->ctx);
        break;
    default:
        assert(0);
    }
}

static void _save_stack(struct coroutine *C, char *top) {
    char dummy = 0;
    assert(top - &dummy <= STACK_SIZE);
    if (C->cap < top - &dummy) {
        free(C->stack);
        C->cap = top-&dummy;
        C->stack = malloc(C->cap);
    }
    C->size = top - &dummy;
    memcpy(C->stack, &dummy, C->size);
}

void coroutine_yield(struct schedule * S) {
    int id = S->running;
    assert(id >= 0);
    struct coroutine * C = S->co[id];
    assert((char *)&C > S->stack);
    _save_stack(C,S->stack + STACK_SIZE);
    C->status = COROUTINE_SUSPEND;
    S->running = -1;
    swapcontext(&C->ctx , &S->main);
}

你可能感兴趣的:(协程二三事(1))