文章大纲
- 引言
- 一、协程Coroutine概述
- 二、协程的优势
- 三、C语言主流的协程库简介
- 四、协程的切换
- 五、libco
- 1、libco概述
- 2、libco 的使用说明
- 2.1、co_create函数创建并初始化协程对象
- 2.2、声明一个协程对象类型的指针
- 2.3、调用co_create函数创建初始化协程对象
- 2.4、coctx_swap进行协程上下文切换
- 3、共享栈和私有栈
- 4、libco的简单应用
引言
协程不是进程或线程,其执行过程更类似于子例程,或者说不带返回值的函数调用。
一个程序可以包含多个协程,可以对比与一个进程包含多个线程,因而下面我们来比较协程和线程。我们知道多个线程相对独立,有自己的上下文,切换受系统控制;而协程也相对独立,有自己的上下文,但是其切换由自己控制,由当前协程切换到其他协程由当前协程来控制。
一、协程Coroutine概述
协程(Coroutine)又名微线程,是一种很早就提出了的程序机制概念,只不过最近几年才在一些程序语言(Lua、Kotlin )上得到广泛应用等。
在协程概念出来之前,在所有的程序语中子程序(又称之为函数或方法)都是基于栈实现层级调用的,函数调用有且只有一个入口和一个返回,在线程内部,调用顺序是明确的自上而下。比如在一个线程内执行函数A,函数A内调用函数B,函数B内又调用函数C,那么执行顺序一定是首先执行A,当执行到B 时,进入函数B 内部,再当执行到C时,执行C,待C返回之后到B内部继续往下执行,然后B返回到A继续往下执行,也就是说传统的函数调用机制中,要想从当前函数调用调到另一个的函数调用只能是主动去执行函数调用。而引入协程概念之后,函数执行过程中,可以产生类似于CPU中断机制,去执行其他函数并且还可以在适当时候返回继续执行。
如上图所示,可以通过yield()函数进行切换,FunctionA执行完Foo()后,执行FunctionB的Foo(),然后再返回来执行FunctionA的Bar(),最后执行FunctionB的Bar()。
二、协程的优势
- 协程具有极高的执行效率,因为子程序切换不涉及到线程的切换,而是由程序自身自主进行控制。避免了线程切换带来的资源消耗。线程越多,协程性能优势越明显。
- 协程不需要考虑线程的锁机制,因为只有一个线程,自然也不存在同时写变量时的数据安全隐患,在协程中不通过锁来控制共享资源,而是通过状态判定。
三、C语言主流的协程库简介
C语言主流的协程库有libtask、libmill、boost、libgo、libco等。
四、协程的切换
- 使用ucontext 系列接口,例如libtask。
- 使用boost.context,纯汇编实现,内部实现机制跟ucontext 完全不同,效率非常高,tbox也基于此。
- 使用setjmp/longjmp 接口,例如libmill。
五、libco
1、libco概述
libco 基于性能考虑没有使用ucontext 系列接口,而是自行编写了一套汇编来处理上下文的切换,具体实现在coctx_swap.s里。libco 在进行上下文切换时只保存和交换了两类东西:寄存器(函数参数寄存器、函数返回值寄存器、数据存储类寄存器等)和栈(rsp栈顶指针)。相较于ucontext,缺少了浮点数上下文(因为在服务端编程几乎用不到浮点数计算,此外libco的上下文切换只支持x86 架构)、sigmask(信号屏蔽掩码,而取消sigmask 是因它会引发一次syscall,在性能上有所损耗)。
x86 架构下libco的性能约是ucontext 的 3.6倍。
libco 牺牲了通用性,把服务端环境下不需要的寄存器拷贝去掉,并对代码进行了极致优化,换取了高效的性能。libco框架图如下:
底层基于i/o多路复用模型实现异步i/o,中间层这是对系统函数的hook,主要是将阻塞的系统调用(如read、write)改为异步调用,最上层是用户接口层,直接提供对应的api,实现了协程原语(协程创建、执行、调度等)并且实现了一套协程间的通信的信号量。核心思想有两点:
- 对协程上下文的切换
- 对同步接口进行异步化转换
2、libco 的使用说明
2.1、co_create函数创建并初始化协程对象
2.2、声明一个协程对象类型的指针
stCoRoutine_t* pointer_produce=NULL;
2.3、调用co_create函数创建初始化协程对象
//函数原型声明
int co_create(stCoRoutine_t **co,const stCoRoutineAttr_t *attr, void *(routine)(void *),void *arg );
//函数调用
co_create(&pointer_produce,NULL,Producer,¶ms);
co_create的函数返回值恒为0,四个参数分别代表:
- stCoRoutine_t **co——出参,二级指针,返回创建的协程对象的地址
- const stCoRoutineAttr_t *attr——入参,用于指定创建协程时需要指定的属性时,使用默认参数则传NULL
- void *(routine)(void *)——函数指针,协程执行的函数
- void *arg——代表协程执行函数时所需要的参数
2.4、coctx_swap进行协程上下文切换
//函数原型
extern void coctx_swap(coctx_t *,coctx_t *)asm("coctx_swap");
coctx_swap 本质上是调用汇编函数,该函数下有两个类型为costx_t 的参数,分别代表挂起和恢复的协程。以下是coctx_t 结构体的实现:
struct coctx_t
{
void* regs[14];
size_t ss_size;
char* ss_sp;
};
本质上所谓协程上下文切换就是做了三件事:
- 保存挂起协程的寄存器信息
- 恢复启动协程的寄存器信息
- 跳转到启动协程的挂起地址继续执行
一般不需要我们开发者直接调用
3、共享栈和私有栈
协程是用户级线程,其共享同一套寄存器,当需要挂起该协程时,需要把其对应的寄存器信息保存起来,regs 就是用于保存寄存器信息的;而ss_sp 则代表执行协程的栈帧信息,libco为每一个协程在堆上分配了128k的空间作为该协程的栈帧。libco 把协程栈分为私有栈和共享栈:
协程在切换时会保存栈内容,多个协程共用一片内存空间,即所谓共享栈,本质上是一个数组,它里面有count个元素,每一个元素都是指向一段内存的指针stStackMem_t,在新分配协程时(co_create_env),它会从刚刚分配的stShareStack_t 中,按一定的方式去一个stStackMem_t 出来,然后算作是协程自己的栈。因此称为共享栈。两种类型的栈各有优缺点:
对于共享栈来说,协程使用的栈空间可以开辟比较大,但每次拷贝需要额外消耗且栈地址不可跨协程使用;而对于私有栈来说,每个协程独立运行与自己独享的栈内存空间中,无需额外的拷贝栈内存且使用安全,但是可能会占用较多的内存(操作系统对大的内存一般只分配地址空间,真实使用时才会触发缺页中断,申请物理内存)。
4、libco的简单应用
void* produce(void* arg);
void* consume(void* arg);
struct stParam_t
{
//条件变量
stCoCond_t* cond;
//数据池
std::vector<int> vec_data;
//数据ID
int data_id;
//协程Id
int coroutine_id;
};
int main()
{
stParam_t p;
p.cond=co_cond_alloc();
p.coroutine_id=p.data_id=0;
srand(time(NULL));
//协程对象(CCB),一个生产者,多个消费者
const int consumer_counts=2;
stCoRoutine_t* producer_coroutine=NULL;
stCoRoutine_t* consumers_coroutine[consumer_counts]={NULL};
//创建并启动生产者协程
co_create(&producer_coroutine,NULL,produce,&p);
co_resume(producer_coroutine);
std::cout<<"start producer_coroutine success!"<<std::endl;
//创建并启动消费者协程
for(int i=0;i<consumer_counts;i++)
{
co_create(&consumers_coroutine[i],NULL,consume,&p);
co_resume(consumers_coroutine[i]);
}
std::cout<<"start consumer_coroutine success!"<<std::endl;
//启动循环事件
co_eventloop(co_get_epoll_ct(),NULL,NULL);
return 0;
}
void* produce(void* arg)
{
//启用协程HOOK项
co_enable_hook_sys();
stParam_t* p=(stParam_t*)arg;
int cid=++ p->coroutine_id;
while(true)
{
//随机产生数据
for(int i=rand()%5+1;i>0;i--)
{
p->vec_data.push_back(++ p->data_id);
std::cout<<"["<<cid<<"] + add new data:"<<p->data_id<<std::endl;
}
//通知消费者
co_cond_signal(p->cond);
//必须使用poll 等待
poll(NULL,0,1000);
}
return NULL;
}
void* consume(void* arg)
{
//启动协程HOOK项
co_enable_hook_sys();
stParam_t* p=(stParam_t*)arg;
int cid=++ p->coroutine_id;
while(true)
{
//检查数据池,无数据则等待通知
if(p->vec_data.empty())
{
co_cond_timedwait(p->cond,-1);
contine;
}
//消费数据
std::cout<<"["<<cid<<"] - dele data:"<<p->vec_data.front()<<std::endl;
p->vec_data.erase(p->vec_data.begin());
}
return NULL;
}
未完待续…