一、协程是什么?协程(Coroutine),源自 Simula 和 Modula-2 语言,是一种程序组件,可以理解为是一种可多次返回的函数:int coroutine(){
dosometing_1();
yield(1);//第一次返回
dosometing_2();
yield(2);//第二次返回
dosometing_3();
return 3;//最终返回
}二、协程有什么用?基于其多次返回特性,协程可用作迭代器、事件分发、SAX解析等。但应用最多的,最能体现协程效果的,还是被称为“用户态线程”的合作式多任务。合作式多任务的主要作用,是可以把令人费解的异步程序,用因果自然的同步方法写出来。例如:费解的异步int onRecv(int value){
dosometing_2(value);
}
void task(){
dosometing_1();
asyncRecv(onRecv);//异步调用onRecv,task返回后释放线程控制权,回掉里重新获取控制权
}清晰的协程void task(){
dosometing_1();
int value=recvViaCoroutine();//协程,释放线程控制权,等真正收到在重新获取控制权
dosometing_2(value);
}由上可见,同样不占用线程等待,协程要比异步来的自然。如果碰到更复杂点的场景,例如task有返回值,甚至task里要顺序执行多次recv,那异步的处理就凌乱了。脑子要爆掉。。所以说异步是反人类的,协程是救世主。三、Sentry2为什么用协程?这个要从Sentry2的设计目标说起,作为高效高可用的监控系统,Sentry2的架构要做到 无锁、高并发、结构简单清晰。之所以不用多线程模式,除避免线程切换开销的原因外,还有全局锁的问题。因全局缓存的存在,所有对其访问都要加锁,不仅拖累了整体效率,还容易造成死锁等问题,增加了系统复杂性。之所以不用异步,原因如上一节所说的那样,异步的因果相反、异步嵌套、异步结果合并、依赖,复杂度太高,代码不易读,不易写,不易debug。而协程,因其同步的思维异步的执行,成为比较完美的选择。每个请求一个协程,如遇IO等耗时操作可yield切换到其他协程RspType OnRequest(const ReqType& req){
CoroutineResult corRet_1=dosometing_SlowIO();
//下面会将线程控制权切换到其他request(yield),dosometing_SlowIO返回或3000毫秒没返回会自动拿回线程控制权(resume)
IORetType ioRet=corRet_1.get(3000);
return RspType(ioRet);
}上面的代码即为目前sentry2处理请求时的大改流程。伪码。其中CoroutineResult::get用于获取异步结果,如果还没有结果,则切换控制权给其他协程。四、如何引入协程?目前主流编程语言中,Go的协程是比较完善的,goroutine\channel的配合相当强大。lua也有较完善的协程支持。Java可以用Kilim。(慎用,未测试)C++的选择比较多,但基本上都是协程这一概念的实现,大都没有达到实用的程度。例如boost.Coroutine,libtask,Protothreads等,只是实现了“多次返回的函数”这一概念。所以,sentry2实现的过程中,基于boost.Coroutine实现了自己的一套协程框架:sentry-coroutine五、协程是怎么实现的?根据控制权的传递方式的不同,协程分为对称协程与非对称协程,他们之间是可互相转换的。我们一般使用对称协程来处理异步问题。协程的实现可分为stackless、stackfull两种。其中stackless一般使用语言的特性来实现,比如被大量使用的switch\case,杰出代表为Protothreads,一个号称蝇量级的协程库。stackless的方式,优点是速度快,损耗小,不会破换栈,相对安全。但缺点也相当明显:依赖宏、使用复杂、无法跨线程、无法传递到子函数。请看下面示例:PT_THREAD(example(struct pt *pt))
{
PT_BEGIN(pt);//起始这里是switch
while(1) {
if(initiate_io()) {
timer_start(&timer;);
PT_WAIT_UNTIL(pt,
io_completed() ||
timer_expired(&timer;));
read_data();
}
}
PT_END(pt);
}另一种称为stackfull的实现方式,一般为ucontext 或 longjmp,需要对函数的执行本质或汇编有一定了解。且其实现依赖于各系统平台。boost.Coroutine即为longjmp实现。ucontext的优点是相对简单稳定,缺点是开销比较大。每次调用要花掉几百个cpu周期。longjmp的优点是开销相对较小,每次调用仅需几十个cpu周期,但缺点是实现复杂,且容易造成缓存命中问题。幸亏boost.Coroutine为我们封装了这些复杂度。sentry2最终选择了boost.Coroutine作为底层实现。六、协程就够了吗?显然,仅有协程的基本实现是不够的。要实现实用化,首先需要解决以下问题:♨ 什么时候创建?♨ 什么时候、怎么切换(yield)?♨ 怎么通过异步、其他线程恢复(resume)?♨ 超时怎么办?限于篇幅及个人精力,这些将在下一篇《Sentry协程调度器的设计与实现》中详细论述。敬请关注。