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,后面再记录一下项目中的实际使用情况。