浅析 I/O 模型及其设计模式

来源:伯乐在线 - 咸菜

链接:http://blog.jobbole.com/104638/

 

前言

 

I/O在软件开发中的重要性无需多言,无论是在操作系统、网络协议、DBMS这种底层支撑软件还是在移动APP,大型网站服务器等应用软件的开发中都是最核心最重要的部分。特别是现在软件服务使用量和数据量爆炸增长的时代,大数据背景下的高可用分布式系统都离不开高效稳定的I/O。本文就简要分析各类I/O模型的演进、基本原理、应用方法、优缺点及其使用场景。文章最后会简要分析两种常用的I/O设计模式。需要说明的是,对于I/O中的许多问题是没有统一、确切的答案的,因此在分析一些问题的时候会根据自己的理解来说明,很有可能和其他书籍或者文章的观点有出入,至于哪种理解更好欢迎交流。

 

文章大纲:

 

1. 阻塞/非阻塞 同步/异步

 

2. I/O中的阻塞/非阻塞 同步/异步

 

3. BIO、伪异步I/O、NIO、AIO四种常用I/O模型及其对比

 

4. Reactor、Proactor两种I/O设计模式及其对比

 

5. 总结

 

使用各种I/O模型实现的时间服务器源代码仅供参考:https://git.oschina.net/wangxu/TimeServer

 

参考《Neety权威指南》

阻塞/非阻塞 & 同步/异步

 

在介绍I/O模型之前需要先理解几个概念,理解了阻塞/非阻塞 & 同步/异步的联系和区别才能理解I/O模型。关于阻塞/非阻塞 & 同步/异步有很多资料的说法不一致,但是也没必要咬文嚼字,只要能够结合实际的例子来理解每种模型的基本原理就可以了。下面就我自己的理解来说一下这个问题。

 

阻塞/非阻塞:首先需要知道阻塞/非阻塞是针对某一个事件(线程/进程)来说的。对于阻塞,如果一个事件在触发一个请求后,由于条件不满足,那么这个事件就会停在这个请求上。拿一个线程来说,一个线程在请求了一个系统调用之后,由于当前不满足执行这个请求的条件,那么这个线程就会停在这个请求上,直到请求执行完毕或者出现异常返回,这个线程阻塞的时候操作系统不会分配CPU时间。对于非阻塞,如果一个事件在触发一个请求后,无论当前是否满足执行请求的条件,都会把结果或者异常返回给请求事件,这个事件不会被阻塞。

 

举个栗子:你想去ATM机取钱,但是前面有人排队,那么这时候你必须要排在后面。在轮到你取钱之前你哪也不能去什么也不能干,只能排队等待。那这就是阻塞式的。那如果你闲ATM人太多,你去了银行大厅到柜台取钱。那么你到了之后,大堂经理会提示你去一张排队号,在你取完排队号之后,你不必非要站在柜台前面等着,你可以坐着玩手机,如果时间够的话你可以上个厕所,吃个饭都是可以的。如果轮到你了,叫号系统就会广播:请xxx号顾客到xxx号窗口办理业务。这时候你听见就可以去取钱了。这就是非阻塞式的。

 

同步/异步:首先需要强调一点,同步/异步是针对多个事件(线程/进程)来说的。拿单个的事件来谈同步/异步是没有意义的。有点类似于操作系统中进程调度中狭义的同步(和资源互斥相对)。如果事件A需要等待事件B的完成才能完成,这种就可以说是同步的。如果事件A的完成需要事件B的执行结果,但是在B完成之前A不会因为B没有完成而等待,而是继续执行,等待B完成之后自动补全A的任务。类似这种的就是异步的。

 

举个栗子:还是取钱的例子,如果你像上面说的那两种方法取钱的话,你还是得自己出马,排队/取号,办理取钱业务然后回家,这种都是同步的。但是如果你办了一张银行的VIP金卡,那好了,给银行打个电话说需要多少钱什么时候需要,然后你可以接着干你的事情,就当这件事情不存在一样。银行的业务员会把你需要的钱自动在合适的时间给你送来。那么万一业务员在路上被抢劫了,银行也会给你打电话通知你。像这种就类似异步的操作。

 

区分阻塞/同步和非阻塞/异步:只要理解了阻塞/非阻塞式针对单一事件,同步/异步是针对多个事件这个核心就能够区分阻塞/同步和非阻塞/异步这两组完全不同的概念。

 

I/O中的阻塞/非阻塞 & 同步/异步

 

理解了这几种不同的概念,下面来具体的看一下这几种概念组合起来在I/O中的应用。

 

同步阻塞I/O:最常用的一个模型是同步阻塞 I/O 模型。在这个模型中,用户空间的应用程序执行一个系统调用,这会导致应用程序阻塞。这意味着应用程序会一直阻塞,直到系统调用完成为止(数据传输完成或发生错误)。调用应用程序处于一种不再消费 CPU 而只是简单等待响应的状态,因此从处理的角度来看,这是非常有效的。在调用 read 系统调用时,应用程序会阻塞并对内核进行上下文切换。然后会触发读操作,当响应返回时(从我们正在从中读取的设备中返回),数据就被移动到用户空间的缓冲区中。然后应用程序就会解除阻塞(read 调用返回)。从应用程序的角度来说,read 调用会延续很长时间。实际上,在内核执行读操作和其他工作时,应用程序的确会被阻塞。

 

阻塞I/O模型

 

同步非阻塞I/O:在这种模型中,设备是以非阻塞的形式打开的。这意味着 I/O 操作不会立即完成,read 操作可能会返回一个错误代码,说明read请求不能立即满足。需要应用程序调用许多次来等待操作完成。这可能效率不高,因为在很多情况下,当内核执行这个命令时,应用程序必须要进行忙碌等待,直到数据可用为止,或者试图执行其他工作。这个方法可以引入 I/O 操作的延时,因为数据在内核中变为可用到用户调用read 返回数据之间存在一定的间隔,这会导致整体数据吞吐量的降低。

 

非阻塞I/O模型

 

异步阻塞I/O:个人觉得谈这种模型的意义不大,以为请求线程已经阻塞,那么异步的作用就不大了。但还有一种理解就是这里的阻塞是通知的阻塞,而不是请求线程的阻塞,也就是一种带有阻塞通知的非阻塞 I/O。在这种模型中,配置的是非阻塞 I/O,然后使用阻塞select 系统调用来确定一个 I/O 描述符何时有操作。使 select 调用非常有趣的是它可以用来为多个描述符提供通知,而不仅仅为一个描述符提供通知。对于每个提示符来说,我们可以请求这个描述符可以写数据、有读数据可用以及是否发生错误的通知。

 

复用I/O模型

 

异步非阻塞I/O:异步非阻塞 I/O 模型是一种处理与 I/O 重叠进行的模型。读请求会立即返回,说明 read 请求已经成功发起了。在后台完成读操作时,应用程序然后会执行其他处理操作。当 read 的响应到达时,就会产生一个信号或执行一个基于线程的回调函数来完成这次 I/O 处理过程。在一个进程中为了执行多个 I/O 请求而对计算操作和 I/O 处理进行重叠处理的能力利用了处理速度与 I/O 速度之间的差异。当一个或多个 I/O 请求挂起时,CPU 可以执行其他任务;或者更为常见的是,在发起其他 I/O 的同时对已经完成的 I/O 进行操作。

 

异步I/O模型

BIO、伪异步I/O、NIO、AIO四种常用I/O模型及其对比

 

BIO/伪异步IO:BIO是阻塞(block)I/O同时也是同步的,就是说BIO是一种同步阻塞I/O模型,在基于传统的同步阻塞I/O模型的开发中,需要服务端监听端口号,然后客户端通过IP和端口号来和服务端建立TCP连接,以同步阻塞的方法进行数据传输。一般使用BIO的服务端设计中是一客户一线程模型的。

 

BIO模型

 

这种模型是有问题的,如果连接客户端较多会大大消耗服务端的资源,如果线程数量超过服务端能承受的最大数量,那么服务器可能会出现很严重的后果。所以出现了伪异步I/O模型,伪异步I/O模型对BIO模型进行了改进,采用了线程池来处理客户连接线程。这样就可以灵活的设置线程池的大小,可以避免服务端资源耗尽的问题。

 

伪异步I/O模型

 

但是伪异步I/O模型并没有从根本上解决客户连接线程的阻塞问题,只是对BIO模型进行了简单的优化。面对巨大的连接客户线程还是存在客户线程阻塞时间较长反应较慢的问题。

 

NIO:NIO是一种非阻塞(non-block)的,同时又是同步的I/O模型。这里只谈一种带有Selector多路复用器的NIO。NIO是使用一个Selector复用器线程来轮询每一个客户端连接,这样就不用阻塞用户线程,同时也不用每个用户线程忙等待。只使用一个线程来轮询I/O事件,这样一来就可以从根本上解决用户线程阻塞的问题。所以NIO模型比较适合高负载、高并发的网络应用。能够充分利用系统资源快速处理请求返回响应消息。NIO适合连接数较多连接时间I/O任务较短的场景,例如即时消息服务器。如果连接数不多而且比较固定,I/O任务较长使用NIO模型就会得不偿失,不仅增加了编程的复杂度而且达不到预期的效果。

 

AIO:AIO是一种异步(Asyncronous)非阻塞的I/O模型,它需要操作系统内核线程的支持,一个用户线程发起一个系统调用请求后就可以继续执行,内核线程执行完系统调用会根据回调函数来完成处理工作。所以AIO模型应该是一种比较理想的模型,因为操作系统内核线程做了一些工作,所以在编程复杂度上AIO要比NIO要简单一些。AIO比较适合连接数较多其I/O任务比较长的场景。

 

借助《Netty权威指南》上的一张表对比一下各个I/O模型的特点:

 

Reactor、Proactor两种I/O设计模式及其对比

 

Reactor:Reactor模式是基于NIO多路复用I/O模型实现的一种常用的模式。在Reactor模式中每个客户连接会注册自己感兴趣的事件,然后Selector多路复用器会轮询每个就绪事件,每到一个事件执行一个事件。Reactor实现了一个被动的事件分离和分发模型,服务等待请求事件的到来,再通过不受间断的同步处理事件,从而做出反应。Reactor比较适合连接数较多但是任务量较小的场景。Reactor实现相对简单,对于耗时短的处理场景处理高效;操作系统可以在多个事件源上等待,并且避免了多线程编程相关的性能开销和编程复杂性;事件的串行化对应用是透明的,可以顺序的同步执行而不需要加锁;事务分离,将与应用无关的多路分解和分配机制和与应用相关的回调函数分离开来。Reactor同时接收多个服务请求,并且依次同步的处理它们的事件驱动程序;但是Reactor不适合执行耗时较长的操作,处理耗时长的操作会造成事件分发的阻塞,影响到后续事件的处理。

 

Proactor:Proactor模式是基于AIO实现的一种高效的I/O设计模式。在Proactor中用户连接请求I/O操作,这时操作系统内核就会调用相应的系统调用来完成请求,内核线程在完成用户的I/O请求后把执行结果放在完成事件队列中,Proactor从完成事件队列中取出结果根据相应的回调处理器来完成对操作结果的相应处理。Proactor实现了一个主动的事件分离和分发模型;这种设计允许多个任务并发的执行,从而提高吞吐量;并可执行耗时长的任务(各个任务间互不影响)。相对Reactor来说,Proactor性能更高,能够处理耗时长的并发场景;可以异步接收和同时处理多个服务请求的事件驱动程序;但是Proactor依赖操作系统对异步操作的支持,各类操作系统对异步I/O支持的实现细节有差异,没有形成统一的标准。

总结

 

没有最好的I/O模型,只有最适合的I/O模型。

  1. da shang
    donate-alipay
               donate-weixin weixinpay

发表评论↓↓