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_t和ucontext_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_t中uc_link指向后继上下文,当前上下文如果执行结束会跳转到uc_link指向的上下文中运行,如果uc_link为NULL那么当前线程直接退出。uc_stack 设置当前上下文的堆栈大小和位置。
其他参数暂时没用到,具体用到之后再补充
int getcontext(ucontext_t *ucp);
初始化ucp结构体,将当前的上下文保存到ucp中
int setcontext(const ucontext_t *ucp);
设置当前的上下文为ucp,setcontext的上下文ucp应该通过getcontext或者makecontext取得,如果调用成功则不返回。如果上下文是通过调用getcontext()取得,程序会继续执行这个调用。如果上下文是通过调用makecontext取得,程序会调用makecontext函数的第二个参数指向的函数,如果func函数返回,则恢复makecontext第一个参数指向的上下文第一个参数指向的上下文context_t中指向的uc_link.如果uc_link为NULL,则线程退出。
void makecontext(ucontext_t *ucp, void (*func)(), int argc, ...);
makecontext修改通过getcontext取得的上下文ucp(这意味着调用makecontext前必须先调用getcontext)。然后给该上下文指定一个栈空间ucp->stack,设置后继的上下文ucp->uc_link.
当上下文通过setcontext或者swapcontext激活后,执行func函数,argc为func的参数个数,后面是func的参数序列。当func执行返回后,继承的上下文被激活,如果继承上下文为NULL时,线程退出。
int swapcontext(ucontext_t *oucp, ucontext_t *ucp);
保存当前上下文到oucp结构体中,然后激活ucp上下文。
如果执行成功,getcontext返回0,setcontext和swapcontext不返回;
如果执行失败,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,最后调用swapcontext,main和child都是输入,将当前的上下文保存到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,后面再记录一下项目中的实际使用情况。