1. 同步异步 & 阻塞非阻塞
同步异步
同步异步关注的是消息通信机制:
同步就是在发出一个调用时,在没有得到结果之前,该调用就不会返回。但是一旦调用返回,就得到返回值了,换句话说,同步是指调用者主动等到这个调用的结果。
异步是指在调用发出之后,这个调用就直接反悔了,所以没有(该调用的直接)返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果,而是在调用发出后,被调用者通过状态、通知来通知调用者,或者通过回调函数来处理这个调用结果。
阻塞非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态:
阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只哟在得到结果之后才会返回。
非阻塞调用是指在不能立刻得到结果之前,该调用就不会阻塞当前线程。
举个栗子
比如你打电话到图书馆管理员处咨询一本书有没有库存,管理员告诉你等一下。如果你期间一直拿着电话等着,管理员查到了跟你说有,查不到就跟你说没有,那么这个过程就是同步的;如果你在等待管理员查询的过程中啥也不做(直到任务完成),就是阻塞的;如果在等待的过程中,你倒了杯水,然后一会就问一下管理员怎么样了,这就是非阻塞;如果管理员告诉你他要找一会,然后就把电话挂了,过一会他打电话来告诉你他找到了,这就是异步。
异步没有阻塞和非阻塞一说。 非阻塞也不等于异步。
很多人喜欢将JDK1.4提供的NIO框架称为异步非阻塞IO,但是,如果严格按照Unix网络编程模型和JDK的实现进行区分,实际上它只能被称为非阻塞IO,不能叫异步非阻塞IO。在早期的JDK1.4和1.5 update10版本之前,JDK的Selector基于select/poll模型实现,它是基于IO复用技术的非阻塞IO,不是异步IO。在JDK1.5 update10和linux core2.6以上版本,sun优化了Selctor的实现,它底层使用epoll替换了select/poll,上层的API并没有变化,我们可以认为是JDK NIO的一次性能优化,但是它仍旧没有改变IO的模型。
JDK1.7提供的NIO2.0,新增了异步的套接字通道,它是真正的异步IO,在异步IO操作的时候可以传递信号变量,当操作完成之后会回调相关的方法,异步IO也被称为AIO。
NIO类库支持非阻塞读和写操作,相比于之前的同步阻塞读和写,它是异步的,因此很多人习惯于称NIO为异步非阻塞IO,包括很多介绍NIO编程的书籍也沿用了这个说法。为了符合大家的习惯,本书也会将NIO称为异步非阻塞IO或者非阻塞IO,请大家理解,不要过分纠结在一些技术术语的咬文嚼字上。
这里补充一个概念,后面可能会用到:
C10K问题: 网络服务在处理数以万计的客户端连接时,往往出现效率底下甚至完全瘫痪,这被成为C10K问题。
(C10K = connection 10 kilo 问题)。k 表示 kilo,即 1000。
2. Unix标准下的IO模型
以接收socket(套接字)输入为例,一个IO调用一般分为两个阶段:
- 数据准备阶段
- 内核空间复制回用户进程缓冲区阶段
依据操作系统在这两个阶段的不同处理方式,可以将IO模型分为阻塞IO模型、非阻塞IO模型、IO复用模型、信号驱动IO模型和异步IO模型。
POSIX(可移植操作系统接口)把同步IO操作定义为导致进程阻塞直到IO完成的操作,反之则是异步IO。从这个角度来看,阻塞IO模型、非阻塞IO模型、IO复用模型(select/poll/epoll)、信号驱动IO模型都属于同步IO,因为阶段2是阻塞的(尽管时间很短)。只有异步IO模型是符合POSIX异步IO操作含义的,不管在阶段1还是阶段2都可以干别的事。按POSIX的描述似乎把同步和阻塞划等号,异步和非阻塞划等号。这里我们不去纠结具体的定义,先来看看通常符合我们普遍认知的这五种IO模型。
这里统一使用Linux下的系统调用recvfrom作为例子,它用于从套接字上接收一个消息,因为是一个系统调用,所以调用时会从用户进程空间切换到内核空间运行一段时间再切换回来。默认情况下recvfrom会等到网络数据到达并且复制到用户进程空间或者发生错误时返回,而第4个参数flags可以让它马上返回。
阻塞IO模型
使用默认参数调用recvfrom,其系统调用一直等数据直到拷贝到用户空间或者发生错误才返回,这段时间内进程(或线程,下同)始终阻塞。A同学用杯子装水,打开水龙头装满水然后离开。这一过程就可以看成是使用了阻塞IO模型,因为如果水龙头没有水,他也要等到有水并装满杯子才能离开去做别的事情。很显然,这种IO模型是同步的。
阻塞式IO模型非阻塞IO模型
改变flags,让recvfrom不管有没有获取到数据都返回,如果没有数据那么一段时间后再调用recvfrom,如此循环。B同学也用杯子装水,打开水龙头后发现没有水,它离开了,过一会他又拿着杯子来看看……在中间离开的这些时间里,B同学离开了装水现场(回到用户进程空间),可以做他自己的事情。这就是非阻塞IO模型。但是它只有是检查无数据的时候是非阻塞的,在数据到达的时候依然要等待复制数据到用户空间(等着水将水杯装满),因此它还是同步IO。
非阻塞IO模型IO复用模型
这里在调用recvfrom前先调用select或者poll,阻塞发生在这两个系统调用(select/poll)中的某一个之上,而不是阻塞真正的IO系统调用上。在下图中,进程阻塞于select调用,同时kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了(数据报套接字变为可读),select返回套接字可读这一条件,这时我们调用recvfrom把所有数据报复制到应用进程缓冲区。
有人将非阻塞IO定义成在读写操作时没有阻塞于系统调用的IO操作(不包括数据从内核复制到用户空间时的阻塞,因为这相对于网络IO来说确实很短暂),如果按这样理解,这种IO模型也能称之为非阻塞IO模型,但是按POSIX来看,它也是同步IO,那么也和楼上一样称之为同步非阻塞IO吧。
这种IO模型比较特别,分阶段。因为它能同时监听多个文件描述符(fd)。这个时候C同学来装水,发现有一排水龙头,舍管阿姨告诉他这些水龙头都还没有水,等有水了告诉他。于是等啊等(select调用中,阻塞),过了一会阿姨告诉他有水了,但不知道是哪个水龙头有水,自己看吧。于是C同学一个个打开,往杯子里装水(recv)。这里再顺便说说鼎鼎大名的epoll(高性能的代名词啊),epoll也属于IO复用模型,主要区别在于舍管阿姨会告诉C同学哪几个水龙头有水了,不需要一个个打开看(当然还有其它区别)。
IO复用模型信号驱动IO模型
首先开启套接字的信号驱动IO功能,通过调用sigaction注册信号函数。该系统调用立即返回,我们的进程继续工作。等内核数据准备好的时候,内核为该进程产生一个SIGIO信号。我们随后既可以在信号处理函数中调用recvfrom读取数据报,并通知主循环数据已准备好待处理,也可以立即通知主循环,让它读取数据报。D同学让舍管阿姨等有水的时候通知他(注册信号函数),没多久D同学得知有水了,跑去装水。是不是很像异步IO?很遗憾,它还是同步IO(省不了装水的时间啊)。
信号驱动IO模型无论如何处理SIGIO信号,这种模型的优势在于等待数据报到达期间进程不被阻塞。主循环可以继续执行,只要等待来自信号处理函数的通知:既可以是数据报准备好被处理,也可以是数据报准备好被读取。
异步IO模型
异步IO的工作机制是:告知内核启动某个操作,并让内核在整个操作(包括将数据从内核复制到用户进程换缓冲区)完成后通知我们。这种模型与信号驱动模型的主要区别在于:信号驱动IO由内核通知我们核实可以启动一个IO操作,而异步IO模型是由内核通知我们IO操作何时完成。
调用aio_read,让内核等数据准备好,并且复制到用户进程空间后执行事先指定好的函数。E同学让舍管阿姨将杯子装满水后通知他。整个过程E同学都可以做别的事情(没有recv),这才是真正的异步IO。
我们调用aio_read函数,给内核传递描述符、缓冲区指针、缓冲区大小(与read相同的三个参数)和文件偏移(与lseek类似),并告诉内核当整个操作完成时如何通知我们。该系统调用立即返回,而且在等待IO完成期间,我们的进程不被阻塞。
异步IO模型《UNIX网络编程》在该模型下的批注:本书编写至此的时候,支持POSIX异步IO模型的系统仍比较罕见。我们不确定这样的系统是否支持套接字上的这种模型。这儿我们只是用它作为一个与信号驱动模式IO模型相比照的例子。
五个IO模型的比较
可以看出,前四个IO模型的主要区别在于第一阶段,它们的第二阶段是一样的:在数据从内核复制到调用者的缓冲区期间,进程阻塞与recvfrom调用(尽管非常非常短)。
前文我们讲过,POSIX操作把IO定义如下:
- 同步IO操作:导致请求进程阻塞,直到IO操作完成
- 异步IO操作:不导致请求进程阻塞
根据上述定义,前四种IO模型的确都是同步IO模型,因为其中真正的IO操作(recvfrom)将阻塞进程。只有异步IO模型财富和POSIX定义的异步IO。
五个IO模型的比较不同的IO模型由于线程模型、API等差别很大,所以它们的用法差异也非常大。附上《netty权威指南》中的一幅对比图,大家有个概念化的了解。
image.png