I/O作为应用系统常用的功能之一,对其有基本的理解是非常有必要的。尤其是在SOA盛行的今天,高效率的I/O是SOA的基础设施RPC框架的基石。本文将针对I/O领域中常见的两组分类:同步与异步,阻塞与非阻塞进行浅析。
在对I/O进行分类之前,我们先来回顾一下I/O操作的原理,这更有助于我们了解同步与异步、阻塞与非阻塞,以及JAVA BIO、NIO、AIO的底层实现。
在这里,我们以更利于我们理解的网络I/O场景作为分析场景(对于Linux操作系统而言,网络I/O和本地磁盘I/O本质上是一样的),一次网络I/O如下图所示:
我们可以看到,这里主要涉及到用户程序、操作系统Kernel、硬件设备三层,整体操作过程为:
我们关注这里边的第三步。首先考虑未就绪时的处理方式,显而易见,操作系统此时有两种可选的方式:
接下来我们考虑当I/O就绪状态下,数据在用户程序与操作系统Kernel间的传递方式(以读取数据为例,写入数据类似),依然有两种可选的方式:
通过上边的描述,我们对操作系统处理I/O请求的原理进行了分析,并针对未就绪状态的两种处理方式(阻塞与非阻塞)、就绪状态进行实际I/O操作的两种处理方式(同步与异步)进行了分析。由于阻塞与非阻塞、同步与异步均为用户程序角度进行的定义,个人认为,通过操作系统I/O原理的角度对其进行理解,会更加准确,也不容易造成混淆。
现在我们从用户程序角度来看一下同步与异步的区别与联系。同步与异步的区别在于调用方发起调用之后,是否在调用返回的时候就已经得到了结果。同步的方式,当调用返回时即得到了结果;而异步的方式则是发起调用之后立即返回,此时并没有得到结果,当结果产生了之后,调用方得到通知,从而获取到结果。
我们可以举个例子方便理解,你的boss让你查询一下从望京A座到故宫的路线,同步的方式是你的boss从向你交待完这件事之后,一直坐在你身边,等你查询完毕之后,得到路径规划之后才离开;异步的方式是,你的boss交待完事情之后,立即切换到别事情,例如开他的会、写他的代码、做他的ppt等,5分钟后他的钉钉响了,获得你发送的路径规划。
在计算机科学领域,同步和异步的使用场景都有许多。比如以控制信息或短数据消息为主的RPC调用场景,通常而言会采用同步的方式,因为相对会简单一些且消息体比较短,同步调用时间比较可控;而对于创建订单等响应时间要求严苛的场景,通常会采用异步的方式,首先保证订单中心成功创建订单,然后再异步写MQ,或者通过数据同步的方式(传输流、订阅MySQL Binlog事件),通知其余系统(用户积分系统、用户操作日志系统、商家服务系统、物流配送系统等),然后订单中心接收其他关联系统的反馈消息,根据其他关联系统是否操作成功来进行相应的处理(重新发起请求追同步,或daily任务追同步)。
二者的比较见下表:
同步 | 异步 | |
调用结束后,是否返回了结果 | 是 | 否 |
优点 | 简单 | 调用过程及时返回 |
缺点 | 当调用时间较长时,大并发调用请求要么采用排队,要么需多线程应对,导致响应时间性能受限(前者)或内存占用大(后者) | 调用方需全面应对调用失败 |
适用场景 | 短消息体传输,调用返回时间可控,响应时间要求不严苛 | 非短消息体传输,调用返回时间不可控,响应时间要求严苛 |
现在我们来看一下阻塞与非阻塞。从操作系统的视角来看,阻塞与非阻塞主要体现在I/O未就绪时,操作系统对用户程序的处理。如果操作系统挂起了用户程序,则为阻塞的方式;如果操作系统直接返回了未就绪状态给用户程序,则为非阻塞的方式。
我们依然以boss让你查询一下从望京A座到故宫的路线为例。阻塞的方式是,从你的boss交待任务之后,他就一直在等待你的回应,无论此时你是否在忙别的事情(绩效3.25妥妥滴~);非阻塞的方式是,你的boss交待完任务之后,如果你正在忙别的事情,他会立即得到“你的下属正在忙”这样的反馈,并可以继续做别的事情,然后他会继续轮询(采用非I/O复用的方式)直到得到结果。
二者的比较见下表:
阻塞 | 非阻塞 | |
当I/O未就绪时的行为 | 挂起,直到I/O就绪 | 直接返回,返回未就绪状态 |
优点 | NA | 当使用I/O多路复用时,单线程或开启与CPU核心数相等的线程数即可 |
缺点 | 由于阻塞,因此需开启多线程应对多个连接,而受限于内存,线程数存在上限; 线程创建与销毁存在开销。 |
NA |
适用场景 | 并发连接少 | 并发连接多 |
上文中在介绍阻塞与非阻塞方式时,提到了I/O多路复用技术。I/O多路复用技术通常是与非阻塞I/O进行搭配的。
如果没有I/O多路复用,非阻塞I/O方式下,当I/O没有就绪时,用户程序会获取操作系统返回的“I/O不就绪”,为了及时得到I/O就绪的反馈,用户程序会不断地进行轮询。这只是一路I/O,为了能够同时监听多路I/O是否就绪,操作系统提供了I/O多路复用机制,封装了对多路I/O的状态监听。
以Linux操作系统为例,有select、poll和epoll三种I/O多路复用机制。
select和poll采用的是轮询的方式,而epoll在select/poll的基础上进行了性能的增强,对于大量并发连接中仅有少量活跃连接的场景下,可以有效地提升CPU的利用率。
在JAVA编程语言中,BIO和NIO均属于同步操作,只不过前者是阻塞的,后者是非阻塞的。而AIO属于异步操作。
BIO比较适用于并发连接比较少的场景,除单机操控本地文件外,目前不是特别常见;
NIO比较适用于并发连接比较多,且通信消息体比较短的场景(以控制信息、短消息体为主的RPC框架);
AIO比较适用于并发连接比较多,且通信消息体比较长的场景(目前很多大厂建设的异步化RPC框架)。
在本博客的其他文章中会分别对JAVA BIO、NIO和AIO进行介绍,并简要介绍一下当前比较主流的JAVA高性能网络I/O框架。