IO模型:同步/异步、阻塞/非阻塞理解

最近一段时间,部门中的公共项目组交付的所有微服务都采用了Netty + WebFlux的Reactor开发模式(之前是Tomcat + Servlet),本人所在的项目组作为上层的业务组,相信不久之后也要跟上这个变革的步伐。最近随着对Netty学习的深入,发现坑越来越多,对底层概念的理解就显得至关重要。藉此机会将自己对IO模型的理解写下来,以作日后查阅。

IO模型分类

概念解释

老生常谈的两种分类方式:同步和异步、阻塞和非阻塞。这里给出明确定义如下:

  • 同步和异步。所谓同步,意思是调用方和被调用方是“状态同步”的,如果被调用方处于发送数据的状态,那么调用方也必然处于接收数据的“等待”状态;否则,称为异步调用,调用方可以在对方传输数据的时候进行其他操作。

  • 阻塞和非阻塞。这个概念指的是调用方发起一次IO调用后,如果被调用方还没有准备好数据时,是立即返回还是等待其准备好数据(并传输完毕)再返回。如果是立即返回称为非阻塞调用,否则为阻塞。

容易看出,同步异步描述的是调用方和被调用方之间的关系,而阻塞非阻塞描述的是调用方发起调用后的状态,二者是不同维度的概念

基本IO模型

根据前面的概念可以得出二者之间的关系:同步调用可以是阻塞或非阻塞的;异步调用一定是非阻塞的。因此得出三种基本IO模型如下:

  1. 同步阻塞调用(Blocking IO)。在被调用方准备好数据并传输完毕之前,线程都处于阻塞等待状态。

  2. 同步非阻塞调用(Nonblocking IO)。如果数据未准备好,立即返回错误不等待;否则等待数据传输完毕后返回内容。根据实现不同,也可以在数据就绪时不返回内容而返回就绪标志,随后调用方再发起获取数据的调用。

  3. 异步非阻塞调用(Asynchronous IO)。无论数据是否准备好都立即返回空,被调用方准备好数据并传输完毕后,通过信号或回调函数通知调用方。对于系统调用来说,AIO的数据传输操作由操作系统内核进程来执行。

除以上3种基本模型外,还有一种常用的模型:

  • IO多路复用(Multiplexing IO)。多路复用技术对NIO进行了增强,可以同时监听多个NIO句柄的状态,每次调用时将轮询所有的NIO句柄并返回所有已就绪的集合。多路复用调用本身也是同步IO。

在linux系统中,多路复用的相关系统调用为select/poll/epoll。理论上多路复用技术也可以使用AIO,称为Proactor模型;基于NIO的多路复用称为Reactor模型。

IO模型分层

对于常见的网络IO和文件IO,在业务层发起IO调用后的流程通常如下:

  1. 从业务代码到框架底层。
  2. 从框架底层到操作系统内核。
  3. 从操作系统内核到IO设备(网卡/磁盘等)。

第一层,取决于框架的实现。通常业务代码都是基于框架、类库来编写的,经过框架的封装,可以改变用户的调用方式。例如一个底层使用同步阻塞式系统调用的操作,通过框架内部创建多线程,可以模拟出异步调用的回调效果。

第二层,取决于所使用的的系统调用。通常所说的IO模型指的就是该层级。大部分系统调用都是同步的,只有一些特殊的API比如Windows的IOCP库是异步调用。Linux的aio库很多都是伪aio,是通过用户态多线程模拟的。

第三层,通常是异步IO。由于DMA的存在,数据从IO设备传输到OS内核内存中的过程不需要CPU参与,传输完毕后才会通知操作系统。

Java的NIO AIO

JDK 1.4以后,Java提供了NIO的类库,通过设置channel.configureBlocking(false)支持同步非阻塞调用;同时也提供了Selector API用于支持多路复用,Selector API在Windows上使用select实现,在Linux下使用epoll实现。

JDK 1.7以后,Java还提供了AIO(NIO2)的类库,在发起IO操作时同时传入回调函数并立即返回,当IO操作完成后回调函数被自动调用,业务层表现为异步IO调用。然而由于linux内核中aio的实现不完善,Java中的AIO在linux上仍是通过epoll实现的(至少JDK 1.7是),是同步IO;在Windows上是通过IOCP实现,是真正的异步IO。

Netty IO模型

最新的Netty 4.x版本,是基于Java NIO封装的Reactor多路复用模型。根据前面的知识可知Netty使用的是同步IO,但我们经常会看到说Netty是异步事件驱动的,这是为何?

其实二者并不矛盾,只是所处的层级不同而已。同步IO指的系统调用层面,框架对OS内核的调用为同步调用。而说Netty是一个异步框架,是指用户的编程模式为异步模式,所编写的处理函数是由Netty在合适的时机自动触发调用。这种回调机制是通过Netty中的事件循环主线程,不断轮询事件队列实现的。

Netty曾经尝试过AIO(Netty.4.0.0.CR3),后来又删除了,主要原因包括:

  1. Linux aio实现不完整,性能没有基于NIO的epoll好
  2. 基于AIO的Proactor线程模型较为复杂,不易理解与维护
  3. 即使是Windows上基于IOCP的AIO,性能也没有比基于NIO的select有明显提升。

作者原话:https://github.com/netty/netty/issues/2515

  • Not faster than NIO (epoll) on unix systems (which is true)
  • There is no daragram suppport
  • Unnecessary threading model (too much abstraction without usage)
  • I told him the fact that windows-select is crappy and he told me that this is not true anymore for Win7 & 8, which was really interesting for me. He said that it was actually very true for XP (probably even Vista), but since 7 the network stack got an update, which was new to me. IOCP still has some advantages but selector based networks are definitely capable of handling a lot of throughput since Win7, which is nice =).

参考链接:
也谈BIO | NIO | AIO (Java版)
怎样理解阻塞非阻塞与同步异步的区别

你可能感兴趣的:(IO模型:同步/异步、阻塞/非阻塞理解)