【学习】协程

Posted on Posted in 计算机1,186 views

多线程的问题

    基于同步处理的模型对开发者来说更简单,能够直观的展现业务流程逻辑,但应用不广,主要原因在于用于实现并发的多进程、多线程机制并不靠谱:

        a. 首先多进程的效率是个噩梦,特别是在同时运行10K以上的进程,系统的额外管理负荷会非常重,较轻的线程能稍微缓解这个问题;

        b. 采用线程也会有新的问题,因为线程是共享地址空间和文件句柄的,所以互斥冲突需要仔细地考虑,即使我们的代码可以通过避免使用锁来规避冲突,也不能规避底层的库函数和系统调用使用的锁。

换一种思路,为什么在网络服务器这样的专用领域,我们要就一定要使用操作系统的通用的进程、线程来进行并发处理呢?

协程

    其实在并发处理领域,还有另外一种选择:协程,也可以称之为纤程或微线程。目前在很多支持分布式开发的语言中自然支持了该功能,比如go、python、lua、ruby等等。

    协程,coroutine(http://zh.wikipedia.org/wiki/%E5%8D%8F%E7%A8%8B),从字面上看就是协作式的进程,目前操作系统支持的多进程、多线程的调度方式是分时、抢占式的,也就是说线程是由系统来进行调度,线程本身没有话语权,随时可能被调度到另外的线程,而协程的调度是合作式的,由协程自己显式决定切换到其他协程执行。这样有几个特点:

    1. 避免了大多数的互斥冲突场景,因为线程里抢占式的调度,导致线程代码需要对临界区进行加锁,否则可能就不知不觉被系统调度到另外一个线程,这个线程会破坏临界区; 而协程不一样,协程只需保证在交出控制权时完成临界区就可以了;

    2. 协程在应用态进行上下文切换,而进程、线程都是在内核态进行上下文切换,协程的切换基本上和调用函数效率接近,比线程切换效率更高;

    3. 线程是1:1的调度模型,即一个线程实体对应于内核的一个调度实体,而协程是N:1的,即多个协程对应于内核里的一个调度实体,从这里看协程的系统管理开销也比线程小;当然这个也是协程的缺点,一个是调试困难,另一个是不支持多核,协程要利用多核其实也可以通过N:M的方式来规避,比如在多个worker进程里启动并发协程来处理请求;

    4. 因为协程是合作式调度,一个协程如果出现死循环,那么整个系统将卡住无法继续工作。当然在现实中,这个问题并不严重,因为首先,在服务器中,运行的代码是可控的,不会意外地出现打扰者;再次,真出现死循环这样的问题,不管是线程、还是协程都需要恢复服务、debug,不是吗?

从上面看,协程自然比线程效率更高,那么,有哪些C/C++领域里可用的协程库可以使用呢。

State Threads库

    C领域有许多基于协程工作原理的库,包括Pth(http://www.gnu.org/software/pth/)、coro、libpcl、State Threads(后文简称ST),其中很多都是设计为通用库,只有ST专门为网络服务器框架进行设计和优化。

    ST库结合了多线程和异步模型的优点,它的API提供了像线程一样的同步编程方式,允许一个并发在一个“微线程”里面执行,这些微线程都在一个进程里面,微线程拥有自己的IP、SP和栈空间,栈空间可以看作是某个并发连接的session。底层的实现和“非阻塞IO和就绪时通知机制”类似,采用IO监听器唤醒那些等待的微线程。(http://state-threads.sourceforge.net/docs/st.html)

    ST库中微线程上下文切换(进程状态改变)只会在一些可能造成睡眠的函数中才会发生(IO点例如send、recv,或者明确的同步点例如sleep)。所以,进程级别的数据不需要锁来保护,因为本身是单线程运行。而整个程序可以自由的使用静态变量和不可重入的函数,极大的简化了编程和调试,从而增加了性能。这实际上是和协程类似,协程是通过显式调用yeild或switch来交出控制权,而微线程是在调用阻塞的IO函数被阻塞时交出控制权。所有的微线程都有同样的优先级,是非抢占式的调度,和SPP的状态机驱动架构类似。这种调度方式很适合网络服务器,因为网络服务器是数据驱动的,处理流程由数据到达的次序决定。

    在上下文切换的具体实现上,ST使用setjmp/longjmp的方法,也针对一些系统架构做了兼容性处理。

    ST库的底层包括一个事件监听器,所有等待事件的微线程都挂在该监听器上,最终由监听器来负责派发到达的事件,这些事件包括网络事件、磁盘IO事件、定时器等。该监听器在不同平台可以选择不同的技术,例如select、kqueue、poll、epoll等等,当然在linux下的首选就是epoll。

    从上面可以看出,ST库底层采用了高效的线程管理方式、IO事件通知方式来实现一个目标:同步编程、异步执行。将两者的优势巧妙地结合在一起。

    当然ST库也有一些缺点,比如需要使用ST库里的IO接口,否则可能会堵塞导致系统性能下降,当然这些问题都有办法克服,因为本身框架就有义务封装IO接口,或者可以像pth那样,将系统调用接管了。


转载标明出处:https://blog.evanxia.com/2016/06/686