两种高效的事件处理模型:Reactor模式和Proactor模式缓存
在高性能的I/O设计中,有两个比较著名的模式Reactor和Proactor模式,其中Reactor模式用于同步I/O,而Proactor运用于异步I/O操做。服务器
在比较这两个模式以前,咱们首先的搞明白几个概念,什么是阻塞和非阻塞,什么是同步和异步?网络
同步和异步是针对应用程序和内核的交互而言的,同步指的是用户进程触发IO操做并等待或者轮询的去查看IO操做是否就绪,而异步是指用户进程触发IO操做之后便开始作本身的事情,而当IO操做已经完成的时候会获得IO完成的通知(异步的特色就是通知)。多线程
阻塞和非阻塞是针对于进程在访问数据的时候,根据IO操做的就绪状态来采起的不一样方式,说白了是一种读取或者写入操做函数的实现方式,阻塞方式下读取或者写入函数将一直等待,而非阻塞方式下,读取或者写入函数会当即返回一个状态值。并发
通常来讲I/O模型能够分为:同步阻塞,同步非阻塞,异步阻塞,异步非阻塞IO异步
同步阻塞IO:在此种方式下,用户进程在发起一个IO操做之后,必须等待IO操做的完成,只有当真正完成了IO操做之后,用户进程才能运行。JAVA传统的IO模型属于此种方式!socket
同步非阻塞IO:在此种方式下,用户进程发起一个IO操做之后边可返回作其它事情,可是用户进程须要时不时的询问IO操做是否就绪,这就要求用户进程不停的去询问,从而引入没必要要的CPU资源浪费。其中目前JAVA的NIO就属于同步非阻塞IO。函数
异步阻塞IO:此种方式下是指应用发起一个IO操做之后,不等待内核IO操做的完成,等内核完成IO操做之后会通知应用程序,这其实就是同步和异步最关键的区别,同步必须等待或者主动的去询问IO是否完成,那么为何说是阻塞的呢?由于此时是经过select系统调用来完成的,而select函数自己的实现方式是阻塞的,而采用select函数有个好处就是它能够同时监听多个文件句柄(若是从UNP的角度看,select属于同步操做。由于select以后,进程还须要读写数据),从而提升系统的并发性!性能
异步非阻塞IO:在此种模式下,用户进程只须要发起一个IO操做而后当即返回,等IO操做真正的完成之后,应用程序会获得IO操做完成的通知,此时用户进程只须要对数据进行处理就行了,不须要进行实际的IO读写操做,由于真正的IO读取或者写入操做已经由内核完成了。目前Java中尚未支持此种IO模型。
搞清楚了以上概念之后,咱们再回过头来看看,Reactor模式和Proactor模式。
(其实阻塞与非阻塞均可以理解为同步范畴下才有的概念,对于异步,就不会再去分阻塞非阻塞。对于用户进程,接到异步通知后,就直接操做进程用户态空间里的数据好了。)
首先来看看Reactor模式,Reactor模式应用于同步I/O的场景。咱们分别以读操做和写操做为例来看看Reactor中的具体步骤:
读取操做:
- 应用程序注册读就绪事件和相关联的事件处理器
- 事件分离器等待事件的发生
- 当发生读就绪事件的时候,事件分离器调用第一步注册的事件处理器
- 事件处理器首先执行实际的读取操做,而后根据读取到的内容进行进一步的处理
写入操做相似于读取操做,只不过第一步注册的是写就绪事件。
下面咱们来看看Proactor模式中读取操做和写入操做的过程:
读取操做:
- 应用程序初始化一个异步读取操做,而后注册相应的事件处理器,此时事件处理器不关注读取就绪事件,而是关注读取完成事件,这是区别于Reactor的关键。
- 事件分离器等待读取操做完成事件
- 在事件分离器等待读取操做完成的时候,操做系统调用内核线程完成读取操做(异步IO都是操做系统负责将数据读写到应用传递进来的缓冲区供应用程序操做,操做系统扮演了重要角色),并将读取的内容放入用户传递过来的缓存区中。这也是区别于Reactor的一点,Proactor中,应用程序须要传递缓存区。
- 事件分离器捕获到读取完成事件后,激活应用程序注册的事件处理器,事件处理器直接从缓存区读取数据,而不须要进行实际的读取操做。
Proactor中写入操做和读取操做,只不过感兴趣的事件是写入完成事件。
从上面能够看出,Reactor和Proactor模式的主要区别就是真正的读取和写入操做是有谁来完成的,Reactor中须要应用程序本身读取或者写入数据,而Proactor模式中,应用程序不须要进行实际的读写过程,它只须要从缓存区读取或者写入便可,操做系统会读取缓存区或者写入缓存区到真正的IO设备.
综上所述,同步和异步是相对于应用和内核的交互方式而言的,同步须要主动去询问,而异步的时候内核在IO事件发生的时候通知应用程序,而阻塞和非阻塞仅仅是系统在调用系统调用的时候函数的实现方式而已。
说到阻塞,首先得说说I/O等待。I/O等待是不可避免的,那么既然有了等待,就会有阻塞,可是注意,咱们说的阻塞是指当前发起I/O操做的进程被阻塞。同步阻塞I/O即是指,当进程调用某些涉及I/O操做的系统调用或库函数时,好比accept()(注意accept也算在了i/o操做)、send()、recv()等,进程便暂停下来,等待I/O操做完成再继续运行。这是一种简单而有效的I/O模型,它能够和多进程结合起来有效的利用CPU资源,可是代价就是多进程的大量内存开销。
同步阻塞:进程坐水,就不能烧粥 同步非阻塞:相似于用一个进程坐水,烧粥。while(true){if… if… }
好处就是一个进程处理多个i/o请求,劣势就是须要不停的轮询。
区别在于等不等待数据就绪。由于数据占了等待的80%时间。同步非阻塞的优点就是一个进程里同时处理多个I/O操做。
在同步阻塞I/O中,进程实际上等待的时间可能包括两部分,一个是等待数据的就绪,另外一个是等待数据的复制,对于网络I/O来讲,前者的时间可能要更长一些。
与此不一样的是,同步非阻塞I/O的调用不会等待数据的就绪,若是数据不可读或者不可写,它会当即返回告诉进程。
好比咱们使用非阻塞recv()接收网络数据的时候,若是网卡缓冲区中没有可接收的数据,函数就及时返回,告诉进程没有数据可读了。相比于阻塞I/O,这种非阻塞I/O结合反复的轮询来尝试数据是否就绪,防止进程被阻塞,最大的好处便在于能够在一个进程里同时处理多个I/O操做。但正是因为须要进程执行屡次的轮询来查看数据是否就绪,这花费了大量的CPU时间,使得进程处于忙碌等待状态。
非阻塞I/O通常只针对网络I/O有效,咱们只要在socket的选项设置中使用O_NONBLOCK便可,这样对于该socket的send()或recv()便采用非阻塞方式。若是服务器想要同时接收多个TCP链接的数据,就必须轮流对每一个socket调用接收数据的方法,好比recv()。无论这些socket有没有能够接收的数据,都要询问一遍,假如大部分socket并无数据能够接收,那么进程便会浪费不少CPU时间用于检查这些socket,这显然不是咱们所但愿看到的。
同步和异步,阻塞和非阻塞,有些混用,其实它们彻底不是一回事,并且它们修饰的对象也不相同。阻塞和非阻塞是指当进程访问的数据若是还没有就绪,进程是否须要等待,简单说这至关于函数内部的实现区别,也就是未就绪时是直接返回仍是等待就绪;而同步和异步是指访问数据的机制,同步通常指主动请求并等待I/O操做完毕的方式,当数据就绪后在读写的时候必须阻塞(区别就绪与读写二个阶段,同步的读写必须阻塞),异步则指主动请求数据后即可以继续处理其它任务,随后等待I/O,操做完毕的通知,这可使进程在数据读写时也不阻塞。(等待”通知”)
多数状况下,Web服务器对这些请求采用基于队列的自由竞争,经过多执行流(多进程或多线程)来充分占 用CPU以及I/O资源,减小任何无辜的等待时间,这其中包括了不少种具体实现的并发策略,在实际应用中,特别是Web服务器,同时处理大量的文件描述符是必不可少的。多路I/O就绪通知的出现,提供了对大量文件描述符就绪检查的高性能方案,它容许进程(好比电子屏,会闻到各个饭馆作好饭菜的味道)经过一种方法来同时监视全部文件描述符,并能够快速得到全部就绪的文件描述符,而后只针对这些文件描述符进行数据访问。
回到买面条的故事中,假如你不止买了一份面条,还在其它几个小吃店买了饺子、粥、馅饼等,由于一块儿逛街的朋友看到你的面条后也饿了。这些东西都须要时间来等待制做。在同步非阻塞I/O模型中,你要轮流不停的去各个小吃店询问进度,痛苦不堪。如今引入多路I/O就绪通知后,小吃城管理处给大厅安装了一块电子屏幕,之后全部小吃店的食物作好后,都会显示在屏幕上,这可真是个好消息,你只须要间隔性的看看大屏幕就能够了,也许你还能够同时逛逛附近的商店,在不远处也能够看到大屏幕。
多路就绪:一、强调多路; 二、只针对请求数据是否就绪,不针对I/O读写。epoll针对的是这样的场景。select, epoll都只须要进程(我)被动接收到数据就绪(面条)”通知”,符合异步的定义。 不须要一直在饭馆等(同步阻塞),或轮询(同步非阻塞)。