其他
技术分享丨微信服务器协程原理解析及优化
直观的想法是,如果我们把RPC也利用epoll+ callback的方式来处理,这样不就高效了吗?
然后我就着手开始进行开发、封装接口,很快就发现了问题所在。这种模式需要注册各种callback,成功的、失败的、超时的等等,把这种复杂性暴露出来很难管理和理解;此外更重要的是还需要一种灵活的保持上下文的机制,如何在调用callback时获得上次请求的上下文、临时变量等等?如何处理各种错误逻辑?即便咬牙实现了,也不够易用,远离了框架的初衷。
Nginx却恰恰是在异步这条路上走到了极致,提到Nginx常见的描述都是异步、高性能、复杂、不好理解等等。基于Nginx模块开发也不失为一个方案。Python的Twisted也是类似的思路,笔者也用过一阵子,感觉比较难用。
后来看到python的gevent(基于greenlet)以及由此接触到协程(coroutine)这个概念,貌似是解决这个问题的银弹。当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。可以认为greenlet增强了python的协程机制,什么是协程呢?
function object也可以有类似的保存上下文的能力,coroutine不同的地方在于每次是从上次暂停的地方开始继续执行,而function object每次是从头开始执行。
此外coroutine和进程、线程也比较像,都可以理解为一种“指令的执行序列”。协程和这俩之间有啥关系和区别呢?
首先,协程对于OS来说透明的,OS只知道进程、线程(统称task),但不知道协程。这意味着对应同一线程的多个协程之间只是伪并发,不能像多线程那样利用多核,因为OS只能调度线程。如果OS不知道协程,那协程是如何被调度、执行的呢?答案是程序员自己控制协程什么时候暂停,什么时候切换、以及切换到哪个协程。
我们知道OS对整个计算机负责,控制着各种权限,开放出一些syscall给process、thread调用访问系统资源,决定下一步执行那个task,保证整个计算机的高效、安全的运行;如果开放出调度的权限给我们,那整个计算机不就乱套了吗?事实上也不会有这种问题,同样是因为OS并不知道coroutine,多个coroutine只会瓜分同一个task的资源(OS分配的cpu时间片之类),至于如何瓜分,就随便程序员你了,OS并不care。
微信的libco专注于利用协程解决RPC情景遇到的问题,通过hooksocket系统调用的方式(类似于python gevent),程序员可以采用同步的写法,写出异步执行的代码。
启用协程后,协程在会阻塞的地方(比如socket read),自动挂起,然后选择执行其他ready的协程执行;未来某个时间协程等待的数据到达时,协程转化为就绪状态,可以重新开始执行。而协程的挂起、继续执行对程序员是透明的。
但这一切的基础是协程的context switch相比线程要是非常高效的,否则利用线程完全可以做同样的事情。
而协程这里要考虑的问题要简单些,一个task内的多个协程谁能跑谁就跑,至于跑多长时间则无所谓,如果有个协程一直能跑那也可以一直不切换。但如果当前正在执行的协程跑不动了(网络IO),那它要主动退出(靠自觉),靠另外一个Manager协程选择另一个能跑的协程继续跑。
Manager如何知道哪个协程能跑呢?这个就要自己实现了,一般协程挂起时,都是由于需要等待一个条件的满足,比如socket的可读事件,如果socket有数据可读就会触发相应的协程由等待状态转化为就绪状态。
由此可见协程是一种cooperative类型的调度策略,而OS的调度一般都是preemptive的,不然有些恶意程序一直while(true)死循环,这个机器就跪了。如何让某个协程开始执行呢?简单说来就是利用getcontext、makecontext、swapcontext来做用户态的contextswitch。
就像在本文开始提到的,我们之所以使用coroutine技术就是期望协程具有比线程更好的context switch性能,因此swapcontext的性能对于协程就至关重要。
Boost::Context、 libco都有自己的实现(汇编代码,跟硬件架构有关)。Boost::Context测试比系统调用的swapcontext性能有几十倍的提升,这样整个coroutine的技术方案终于make sense了。
我们都知道线程的引入带来了很多好处,多线程向进程内部引入了额外的并发,线程之间共享地址空间、共享打开的文件。这些并发、共享带来了高效率,但恰恰由于这些并发、共享,多线程开发就需要额外注意共享资源之间的同步问题,需要引入线程锁、原子操作等复杂性来保证多线程程序的正常操作。
类似地,协程进一步向线程内部引入了并发,这时线程局部变量对协程来说也是共享的,同样需要注意并发安全的问题。
我觉得最好要保证函数是reentrant的,因为有可能一个协程在函数内部挂起,然后另一个协程重新进入这个函数。这就意味着要特别注意对全局变量、静态变量的使用。还要特别注意锁的使用。libco提供了协程局部变量,使这个问题容易解决一些。其实为了处理协程之间访问共享变量的竞争的问题,只要不发生协程切换就好了,对于libco就是不发生网络IO。
python的协程可以看看这个pdf:
"A Curious Course on Coroutines and Concurrency "
http://www.dabeaz.com/coroutines/Coroutines.pdf
libco的代码是公司内开源的,阅读下也挺好的。
经验分享丨项目实践项目孵化丨渠道发行做有梦想的游戏人-GAME AND DREAM-