C++中的协程(上)

c++中协程的使用

我们的项目在逻辑中的异步操作并不太多,而且使用异步本身是一件很麻烦的事情,mysql也没有封装异步的方法,但是后期一些项目上的功能需要使用异步sql查询数据库,为了适应项目的发展和需求,引入了协程。

从原理上看,协程保存了程序执行的当前位置,后续可以切换回来,即便是在一个完整函数的中间,也可以切走再切回来,像是一个用户态的线程,但是和线程不同的是,协程之间的切换需要我们自己实现。协程的好处是可以用符合思维习惯的同步写法,写出具有异步功能的高效代码,而不用像传统异步开发那样,设置各种回调函数把代码割离,弄的支离破碎很难理解。不过协程的切换虽然不会切到内核态,完全由用户来控制切换,但是协程的切换造成的开销,还是会比异步回调的方法开销大一些,但是总体来说,同步的写法实现异步这种好处还是盖过了切换带来的效率损耗。

因为c++并没有标准的协程库,所以各种实现协程的方法很多,当前比较知名的是仿go语言实现的c++库libgo,腾讯的libco,boost的coroutine,context组件,unix系统自带的ucontext等等。 这些库我只看过libco和ucontext,腾讯的libco封装的太高级,其实用到自己项目中是不太灵活的,但是libco用汇编重写了切换协程的代码,在这方面的效率是比较高的,我们项目中用了unix系统的ucontext函数,实现了基本的协程功能,在实际使用中效率不太高,但是也足够用。 记录一下ucontext的原理和使用。

ucontext

先看一个简单的例子,来自维基百科:

#include <stdio.h>
#include <ucontext.h>
#include <unistd.h>

int main(int argc, const char *argv[])
{
    ucontext_t context;
    getcontext(&context);
    puts("Hello world");
    sleep(1);
    setcontext(&context);
    return 0;
}

这个程序的运行结果是这样的:

[shafeng@localhost Desktop]$ vim test.cpp
[shafeng@localhost Desktop]$ g++ test.cpp -o test
[shafeng@localhost Desktop]$ ./test 
Hello world
Hello world
Hello world
Hello world
Hello world
Hello world
Hello world
Hello world
Hello world
Hello world
Hello world
^C
[shafeng@localhost Desktop]$ 

getcontext得到了当前的上下文,然后输出Hello world,然后调用setcontext,又跳到了调用getcontext的位置,接着输出Hello world,一直循环下去。。。。


ucontext很简洁,头文件 <ucontext.h> ,两个结构体mcontext_tucontext_t
四个函数getcontext(),setcontext(),makecontext(),swapcontext(),这些内容就可以实现协程。

mcontext_t应该是系统需要传递的参数,是用户无关的,用户只需要关心ucontext_t结构和
getcontext(),setcontext(),makecontext(),swapcontext(),下面详细介绍一个结构体和四个函数:

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

ucontext_tuc_link指向后继上下文,当前上下文如果执行结束会跳转到uc_link指向的上下文中运行,如果uc_linkNULL那么当前线程直接退出。
uc_stack 设置当前上下文的堆栈大小和位置。
其他参数暂时没用到,具体用到之后再补充

int getcontext(ucontext_t *ucp);

初始化ucp结构体,将当前的上下文保存到ucp中

int setcontext(const ucontext_t *ucp);

设置当前的上下文为ucpsetcontext的上下文ucp应该通过getcontext或者makecontext取得,如果调用成功则不返回。如果上下文是通过调用getcontext()取得,程序会继续执行这个调用。如果上下文是通过调用makecontext取得,程序会调用makecontext函数的第二个参数指向的函数,如果func函数返回,则恢复makecontext第一个参数指向的上下文第一个参数指向的上下文context_t中指向的uc_link.如果uc_linkNULL,则线程退出。

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

makecontext修改通过getcontext取得的上下文ucp(这意味着调用makecontext前必须先调用getcontext)。然后给该上下文指定一个栈空间ucp->stack,设置后继的上下文ucp->uc_link.

当上下文通过setcontext或者swapcontext激活后,执行func函数,argcfunc的参数个数,后面是func的参数序列。当func执行返回后,继承的上下文被激活,如果继承上下文为NULL时,线程退出。

int swapcontext(ucontext_t *oucp, ucontext_t *ucp);

保存当前上下文到oucp结构体中,然后激活ucp上下文。

如果执行成功,getcontext返回0,setcontextswapcontext不返回;
如果执行失败,getcontext,setcontext,swapcontext返回-1,并设置相应的errno.

简单说来, getcontext获取当前上下文,setcontext设置当前上下文,
swapcontext保存当前上下文并切换上下文,makecontext创建一个新的上下文。

#include <ucontext.h>
#include <stdio.h>

void func1(void * arg)
{
    puts("func1");
}
void context_test()
{
    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);//修改上下文指向func1函数

    swapcontext(&main,&child);//保存当前上下文到main,切换到child上下文
    puts("main");//如果设置了后继上下文,func1函数指向完后会返回此处
}

int main()
{
    context_test();

    return 0;
}

这个例子中,getcontext首先获得了当前的上下文,child参数相当于一个输出,通过getcontext函数把当前上下文的信息写入到child中。然后修改了child上下文的栈空间和大小,以及后继上下文,然后调用makecontext,这时child相当于输入,将child上下文的执行位置指向func1,最后调用swapcontextmainchild都是输入,将当前的上下文保存到main中,然后切换到child上下文(此时的child上下文位置是func1函数的起点)。 这样这个程序执行后的输出就是:

[shafeng@localhost Desktop]$ vim test.cpp
[shafeng@localhost Desktop]$ g++ test.cpp -o test
[shafeng@localhost Desktop]$ ./test 
func1
main
[shafeng@localhost Desktop]$ 

可以看到当程序执行到swapcontext后,程序通过child上下文跳转到了func1,先执行了puts("func1"); 函数执行结束后跳转到child的后继上下文main,也就是调用swapcontext时保存起来的上下文,这样程序又回到了swapcontext执行结束的位置(因为swapcontext执行成功不返回,swapcontext已经执行成功,所以跳转回来之后程序的位置在swapcontext的下一句)并执行puts("main");

上面的程序在执行完child上下文后能够跳到main上下文是因为设置了child的后继上下文child.uc_link = &main,如果我们把这一句改为child.uc_link = NULL,编译执行后得到如下结果

[shafeng@localhost Desktop]$ vim test.cpp
[shafeng@localhost Desktop]$ g++ test.cpp -o test
[shafeng@localhost Desktop]$ ./test 
func1
[shafeng@localhost Desktop]$ 

可以看到child上下文在执行结束之后就直接退出了线程。 所以上下文的后继上下文很重要,否则我们的程序可能直接退出。

总结

以上就是ucontext的全部内容,非常简洁。我们项目中使用的就是ucontext,后面再记录一下项目中的实际使用情况。