近几年来,协程在 C/C++ 服务器中的解决方案开始涌现。本文主要阐述以汇编实现上下文切换的协程方案,而且说明其在异步开发模式中的应用。python
本文地址:http://www.javashuo.com/article/p-zzjqpbzk-eq.html编程
首先,咱们来看一下 C/C++ 服务器开发的历史。segmentfault
长期以来,使用 C/C++ 编写服务器程序的时候,每每使用的是多进程模式:一个父进程负责 accept
传入链接,而后 fork
一个子进程处理;或者是一个父进程建立了一个 socket
以后,fork
出多个子进程同时执行 accept
和处理。服务器
为何说这是同步呢?由于这个设计思路,彻底就是教科书般的、对于 socket 处理的思路。这个思路我在我关于 libev 的介绍文后的评论中也说起:并发
程序执行的每一步系统调用,都会阻塞住(直接的结果就是致使进程切换),等待远端机器的响应,而且直到数据到达以后,才会执行下一步。这就是典型的同步(阻塞) I/O。框架
上面的每步流程若是简单写下来的话,支撑不起高并发,由于阻塞的存在。为了解决这个问题,加入 fork
,就能够实现对多个客户端的服务了。异步
同步 I/O 框架,使用的是同步开发模式。人的思惟,是同步化的,先作什么后作什么,都是一条线式的流程。这样一条线从头至尾的开发模式,就是同步开发模式,很是符合人的思路习惯,便于设计、理解。socket
exit
退出的话,那么几乎不用考虑内存泄露的问题——进程建立的全部资源都会被操做系统回收。fork
设计进程间切换,这是一个须要陷入内核的操做,耗时长,对于高并发场景,对服务器资源的利用效率很低。吐槽一下,本人进入工做后就见到的第一个服务器就是基于 libevent 设计的,而且整个团队都一直这么设计,以致于我曾经觉得同步 I/O 根本没人用……函数
首先讲从技术层面的 “异步 I/O 框架” 是怎么回事。维基百科上对 “异步 IO” 的定义是:高并发
也就是说,某个进程 or 线程,须要告诉操做系统:我须要在某个文件描述符或句柄上 read
或 write
,可是进程 or 线程并不等待 read
或 write
ready,而是等到真正有数据可读或可写入数据的时候,再执行相应的操做。
其实 read
/ write
一早就在理论上对这样的操做提供了支持,那就是 O_NONBLOCK
标志。当对 socket 设置了该标志后,若是执行 read / write,资源暂时不可用的话,会返回响应的错误。此时,程序就能够跳过这个句柄,去查看下一个资源了。
但这个方案显然是不现实的,由于当客户端数量很大的时候,对全部的资源都须要进行轮询操做,这是对 CPU 时间的极大浪费,也极大地拉低了服务的响应速度。所以,操做系统须要提供定义的后半段:“通知”。
实现 “通知” 的办法,其实就是一个系统调用:select
。其实 select 的效率很低,通常操做系统会提供替代。对于 Linux 而言,就是 epoll
。关于异步 I/O 原理和编程,个人文章有不少了,能够点击这里查看。
从技术层面上,异步 I/O 框架有如下的优点:
然而,单线程多任务其实也是很大的一个劣势——多个任务都在一个线程 / 进程中处理,若是程序有 bug,那么整个进程都会崩溃,这对服务器的开发质量要求很高。
异步 I/O 框架,大部分使用的就是异步开发模式。咱们先不用这个词汇吧,换成你们比较熟悉的词。下面两个词,其实均可以解释什么叫异步开发模式:
异步开发模式它是基于事件驱动的,当什么事件到来,就调用哪一个回调进行处理——或者是回调判断发生了什么事件,再调用不一样的函数处理。这与咱们传统的思惟不一样,所以很大程度上,咱们须要画状态机,才能很好地解释咱们的软件逻辑。
其实,异步开发的世界中,尽是各类回调以及回调的注册。若是咱们不是相应的业务代码的开发者,那么走读代码时,看到一段函数执行完后,咱们根本不知道这段函数的调用方是谁,从而也就没法跟踪判断下一段代码是什么。
这就给调试带来了极大的困难。其实即使是程序的开发者,若是文档不足的话,当时间长了以后,恐怕也会忘记本身当时的业务逻辑了吧……所以,异步开发模式对开发者的水平和团队编程风格的要求很高。
可是异步开发模式也有很大的优势,那就是状态机编程。这其实很好理解,对于那种逻辑并非一整条简单的直线,而是有着很是多的分叉——有不少外部触发条件、而且会致使不少不一样状态切换的程序而言,异步开发模式简直是福音。
好比电梯,一个正运行中的电梯,其执行逻辑很容易被某一楼层用户按下按钮这一动做中断。电梯须要对用户的操做进行及时的响应,以决定本身接下来应该采起什么操做。
一个电梯,至少有如下几个阶段,中断可能发生在电梯运行中的任何一个阶段:
此外,中断的类型还多是多种多样:
若是使用同步开发模式,这样的逻辑简直是灾难!
前文我刻意将同步开发模式和同步 I/O、异步开发模式和异步 I/O 分开来讲明。确实,开发模式和技术手段是两码事。在逻辑比较线性的(相比起上面 “电梯” 的例子)服务(特别是海量服务)而言,咱们最理想的开发方案就是:
曾经我觉得这二者的结合在 C/C++ 上是没法实现的,直到我换了东家以后才知道,原来能够这么玩——
协程,做为一种服务器组件,在多种高级语言中存在。相比起线程和进程而言,它的切换很是速度快(不用陷入内核态,没有系统调用),很适合在海量服务中使用。
可是在以 C/C++ 为主的中级语言服务器开发中,一直没有大规模引入。缘由是,C/C++ 实在是太接近底层了,汇编后的目标文件,直接就是汇编语言代码;而汇编语言的下面,则直接就是虚拟内存了,可以对其施加影响的,只有操做着更加底层(硬件寄存器)的操做系统。
可是其余高级语言不一样,好比 Java
。Java 在原理上是解释型语言,可是从开发者的角度,其实和编译型语言无异,只是它把代码编译成了由 JVM
能够识别的程序罢了。这样,在真正执行的程序(二进制代码)和程序代码之间,JVM 能够提供一个中间层——以往由操做系统执行的任务调度和上下文切换,JVM 能够接管过来,在用户态中完成。这就是协程的实现。
协程的实现,涉及两个内容:
协程调度的原理,往大了说,其实和线程 / 进程的调度原理无异。这里分抢占和非抢占两种了。对于 C/C++ 而言。
要实现抢占式很难,并且也没太大必要,由于花了很大力气实现抢占式的协程调度,反而失去了前文提到的 “同一线程中没有同步问题” 这一优点了。
因此,针对 C/C++ 协程,最好的方式就是使用非抢占式调度,须要任务经过某些调用主动让出 CPU 使用权。再进一步具体化到服务器编程中,因为每个合法的传入链接的优先级是相同的,所以只须要使用基于 epoll
的实现来进行简单调度就好了。
上下文切换,是 C/C++ 协程的一大难题,这也是致使了 C/C++ 长期没有可用的、统一的协程库的缘由。这一部分行文比较长,我仍是放在下一篇文章里面讲吧。