I/O操作 -- 同步与异步,阻塞与非阻塞

背景

I/O作为应用系统常用的功能之一,对其有基本的理解是非常有必要的。尤其是在SOA盛行的今天,高效率的I/O是SOA的基础设施RPC框架的基石。本文将针对I/O领域中常见的两组分类:同步与异步,阻塞与非阻塞进行浅析。

I/O操作的原理

在对I/O进行分类之前,我们先来回顾一下I/O操作的原理,这更有助于我们了解同步与异步、阻塞与非阻塞,以及JAVA BIO、NIO、AIO的底层实现。

在这里,我们以更利于我们理解的网络I/O场景作为分析场景(对于Linux操作系统而言,网络I/O和本地磁盘I/O本质上是一样的),一次网络I/O如下图所示:

I/O操作 -- 同步与异步,阻塞与非阻塞_第1张图片

我们可以看到,这里主要涉及到用户程序、操作系统Kernel、硬件设备三层,整体操作过程为:

  1. 用户程序A发起I/O操作;
  2. 操作系统Kernel通过底层硬件设备检查I/O是否就绪(对于写入方,是否与接收方已完成通信链路连接;对于接收方,是否有数据到达);
  3. 如果未就绪,则进行未就绪时的处理方式(下边会说);如果已就绪,对于接收方,将数据从硬件设备读取到操作系统Kernel,然后再从操作系统Kernel读取到用户程序空间,完成读取操作;对于写入方,将数据从用户程序空间写入操作系统Kernel,然后再将操作系统Kernel中的数据写入硬件设备,以进行向远端的网络传输。

我们关注这里边的第三步。首先考虑未就绪时的处理方式,显而易见,操作系统此时有两种可选的方式:

  1. 操作系统挂起用户程序,切换到其他操作,等就绪了再唤醒用户程序:从调用方的角度看,此时用户程序的调用被阻塞了;
  2. 操作系统不挂起用户程序,直接返回未就绪状态给用户程序:从调用方的角度看,此时用户程序的调用并没有被阻塞,只是状态为未就绪而已。

接下来我们考虑当I/O就绪状态下,数据在用户程序与操作系统Kernel间的传递方式(以读取数据为例,写入数据类似),依然有两种可选的方式:

  1. 用户程序等待操作系统Kernel将数据从硬件设备读取操作系统Kernel,然后再将数据从操作系统Kernel读取到用户程序空间:从用户程序的角度来看,发起读取数据的调用之后,在调用返回时,就已经获取了需要读取的数据,此为同步;
  2. 用户程序发起读取操作之后,无需向1那样等待数据读取完成,而是直接返回。当操作系统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多路复用机制,封装了对多路I/O的状态监听。

以Linux操作系统为例,有select、poll和epoll三种I/O多路复用机制。

select和poll采用的是轮询的方式,而epoll在select/poll的基础上进行了性能的增强,对于大量并发连接中仅有少量活跃连接的场景下,可以有效地提升CPU的利用率。

JAVA I/O方式

在JAVA编程语言中,BIO和NIO均属于同步操作,只不过前者是阻塞的,后者是非阻塞的。而AIO属于异步操作。

BIO比较适用于并发连接比较少的场景,除单机操控本地文件外,目前不是特别常见;

NIO比较适用于并发连接比较多,且通信消息体比较短的场景(以控制信息、短消息体为主的RPC框架);

AIO比较适用于并发连接比较多,且通信消息体比较长的场景(目前很多大厂建设的异步化RPC框架)。

在本博客的其他文章中会分别对JAVA BIO、NIO和AIO进行介绍,并简要介绍一下当前比较主流的JAVA高性能网络I/O框架。

你可能感兴趣的:(JAVA)