1.概述
花了点时间理解了一下协程,可能刚开始看到协程两个字会以为是什么新的事务,它其实用一组特殊的函数就可以实现,并没有深入内核。对于线程,它们的切换是由操作系统根据其系统内核中的调度器,抢占式地进行的,就各个线程间的运行的先后顺序是不能完全控制的。对于协程,它们的切换是由开发者,或者编程语言决定的,也就是说,各任务之间可以通过在某些控制点执行暂停、恢复函数,来达到多任务“协作”的目标的,它的运行的先后是可以完全由用户来控制的。这是协程和线程最直观的差异。再形象点来说协程就是有一个记忆能力的东西,在它运行过程中的任意时候它都能跳到其他函数运行,完了后又能回到之前那个地方。所以我认为协程就是普通调用的升级,为什么这么说,看下文
协程的关键特点是调度/挂起可以由开发者控制。协程比线程轻量的多。在语言层面实现协程是让其内部有一个类似栈的数据结构,当该协程被挂起时能够保存该协程的数据现场以便恢复执行。 在汇编层面理解,协程无非就是jump而已,只不过要把当前的环境保存。流程大致是保存当前的环境,然后跳转到另一个环境中。执行完再跳回来,在保存的环境中继续执行。
跟普通的函数调用很像,他们的区别是协程可以保存当前环境的所有信息,而函数调用只是把当前运行的下一条指令地址压入栈,然后又参数入栈,运行函数,然后出栈,读取之前的指令地址,继续返回运行。所以协程可以在主协程和子协程间跳来跳去,普通的函数调用只能是在一个函数里调用另一个函数,等另一个函数运行完后再返回原函数。来看看下面的例子
#include "ucontext.h"
#include <iostream>
using namespace std;
ucontext_t parent,child;
void Fun2(void* arg)
{
for(int i = 0;i < 5; i++){
cout<<"child "<<i<<endl;
swapcontext(&child,&parent);
}
}
void Fun1(void* arg)
{
char stack[1024];
getcontext(&child);
child.uc_stack.ss_sp = stack;
child.uc_stack.ss_size = sizeof(stack);
child.uc_link = &parent;
makecontext(&child,(void(*)(void))Fun2,0);
for(int i = 0;i < 5; i++){
cout<<"parent "<<i<<endl;
swapcontext(&parent,&child);
}
}
int main()
{
Fun1(0);
return 0;
}
可以看到可以实现在Fun1和Fun2间来回跳转,里面用到了协程相关的函数
getcontext,makecontext,swapcontext
,我们来看看这些函数的实现功能
2.具体实现
下面来看看具体怎么实现协程(注:主要是利用 ucontext组件)
ucontext里有个主要的结构体ucontext_t
typedef struct ucontext_t
{
unsigned long int __ctx(uc_flags);
struct ucontext_t *uc_link;
stack_t uc_stack; //上下文中使用的栈
mcontext_t uc_mcontext; //上下文的特定寄存器
sigset_t uc_sigmask; //上下文中阻塞信号集合
struct _libc_fpstate __fpregs_mem;
} ucontext_t;
其主要就是保存程序某一个节点的环境即上下文。另外提供了四个函数
//获取当前ucontext_t
int getcontext(ucontext_t *ucp);
//切换到指定ucontext_t
int setcontext(const ucontext_t *ucp);
//设置函数指针和堆栈到对应ucontext_t保存的sp和pc寄存器中
void makecontext(ucontext_t *ucp, void (*func)(), int argc, ...);
//保存当前环境到oucp,并且切换到指定ucp
int swapcontext(ucontext_t *oucp, ucontext_t *ucp);
使用流程一般是先用getcontext获取当前环境,然后用makecontext绑定对应的执行函数,然后swapcontext跳转到协程执行其函数,执行后自动有回到当前环境,简单的演示代码如下
void Func(void* arg)
{
printf("child\n");
}
void test()
{
char stack[1024];
ucontext_t child, parent;
//获取当前上下文
getcontext(&child);
//修改当前上下文
child.uc_stack.ss_sp = stack;
child.uc_stack.ss_size = sizeof(stack);
//设置执行完后返回的后继上下文
child.uc_link = &parent;
//给child指定执行函数
makecontext(&child,(void(*)(void))Func,0);
//保存当前上下文到parent中,激活child去运行其中要执行的函数
swapcontext(&parent,&child);
printf("parent\n");
}
int main()
{
test();
return 0;
}
3.深入理解
继续往下深入理解,来看看具体是如何实现协程间的切换的
从提供的函数中可以看到协程主要是有两个状态,运行和挂起,当用swapcontext是当前协程从从RUNNING 转换为 COROUTINE_SUSPEND,主要需要做的是保存当前运行栈,其中调用_save_stack 来保存运行栈
static void _save_stack(struct coroutine *C, char *top) {
char dummy = 0;
//top 是栈顶,top - &dummy是协程私有栈空间大小
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;
/*从栈底到栈顶的数据全都拷到 C->stack 中
* 就是将当前运行栈保存到该协程私有栈中
*/
memcpy(C->stack, &dummy, C->size);
}
可以看到每个协程都可以有自己的私有栈空间来保存执行现场,协程的函数是保存在公共栈空间的,top 表示栈顶,而dummy表示该用户协程当前的栈底,所以 top-dummy 就表示该用户协程运行时栈所占空间。然后将运行时栈的数据全部拷贝到该协程的C->stack中
再来看协程从 SUSPEND 到 RUNNING 状态恢复栈数据,由于C 的运行时栈空间始终是在 S->main中的,因此恢复栈空间,其实就是将各自私有的C->stack 空间中的数据恢复到S->main
memcpy(S->stack + STACK_SIZE - C->size, C->stack, C->size);
所以每个协程私有栈就是从主栈里划分出一部分来保存当前环境,恢复时就将这部分拷贝到运行空间. so easy
总述:其实在c++异步编程里用函数回调跟协程作用差不多,用协程可以使代码更优雅,同时在多线程编程的有些情况下可以用协程来减少线程数,降低资源消耗,提高性能,因为它就不需要陷入内核来做线程切换,避免了大量的用户空间和内核空间之间的数据拷贝,最近在写高并发服务器,后续试试将工作线程池改为协程,就先写到这里了