理解阻塞非阻塞与同步异步

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/canot/article/details/72590556

当你去查阅Node相关的资料时,经常会看到异步,非阻塞,回调, 事件等关键字,于是你会感觉好像异步与非阻塞是一回事。从开发者的角度来看异步与非阻塞都实现了并行IO的目的,但从操作系统内核来看,阻塞非阻塞与同步异步是有着本质的区别。

本文笔者根据自己熟悉的Java和JS两种语言来作这方面的解释。首先先通过IO流读取文件的操作来两种语言的区别:

NodeJS

var fs = require('fs');
fs.readFile('a.txt','utf8',function(err,data){
    console.log(data);
});
xxxx

Java

InputStream in = new FileInputStream("a.txt" );
byte bu [] = new byte[1024];
int len =0;
while ((len =in .read(bu ))!=-1){
     System. out .println(new String(bu ,0,len ));
}
xxxx

分析上述两段代码,Java的编写很符合大部分人的阅读编写习惯,一直等到文件读完才执行代码。但NodeJS就不一样了,它调用读文件的readFile方法后给参数3又指定了一个函数,因此当你运行这段代码后,会出现readFile()方法执行的代码都在执行了,而打印文件内容的代码没有执行。这就是Node的特性异步操作,同理,Java就是同步操作。

阻塞非阻塞

上述的IO操作对于操作系统而言,仅仅有两种模式:阻塞与非阻塞。
阻塞和非阻塞关注的是线程程序在等待调用结果(消息,返回值)时的状态。
调用阻塞IO时,应用程序需要等待内核的返回,如下图所示:

这里写图片描述
阻塞IO一定是等待OS内核完成磁盘寻道,读取数据,复制数据到内存中之后才返回给应用进程。阻塞IO造成CPU等待IO,减低了CPU的执行效率。因此,为了提高性能,内核提高了非阻塞IO,与阻塞IO的区别就是调用之后OS会立即返回。
非阻塞IO的立即返回是不携带数据的,返回的意义是为了避免浪费CPU的资源。但此时非阻塞IO操作并没有完成,立即返回的也不是应用程序需要的数据而仅仅是当前调用的状态。为了获取完整的数据,应用程序就需要重复调用IO操作来确认是否完成,这个重复判断操作是否完成的过程就叫轮询。
任何技术都不是非常完美的,阻塞IO导致CPU等待浪费,而非阻塞IO却引入了轮询问题。我们先看轮询技术是如何演进的:

read

最原始 ,性能最低的方法,通过重复调用来检查IO的状态来完成数据的获取
这里写图片描述

select

select是在read的基础上改进的一种方案,通过对文件描述符上的事件状态来进行判断。select轮询具有一个较弱的限制,那就是由于它采用一个1024长度的数组来存储状态,所以它最多可以同时检查1024个文件描述符。
这里写图片描述

poll

方案较select有所改进,采用链表的方式避免数组长度的限制,其次它能避免不需要的检查。但是当文件描述符较多的时候,它的性能还是十分低下的。
这里写图片描述

epoll

epoll是Linux下效率最高的I/O事件通知机制,在进入轮询的时候如果没有检查到I/O事件,将会进行休眠,直到事件发生将它唤醒。它是真实利用了事件通知、执行回调的方式,而不是遍历查询,所以不会浪费CPU,执行效率较高。
这里写图片描述
总结就是:阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

同步异步

同步异步与阻塞/非阻塞不同的是,它是在编程语言的API层次上,是供开发者来使用的,与API到底是阻塞/非阻塞的调用OS是没有关系的。
同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication)
所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。如前面Java例子代码中的read方法。换句话说,就是由调用者主动等待且会一直等待这个调用的结果。
而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。如前面的NodeJs例子,调用fs.readFile之后,是通过回调函数来处理。

理想的异步IO

完美的异步I/O应该是应用程序发起非阻塞调用,无须通过遍历或者事件唤醒等方式轮询,可以直接处理下一个任务,只需在I/O完成后通过信号或回调将数据传递给应用程序即可。

异步IO的实现

Node,Java作为两种类型的语言:单线程与多线程对于异步IO的实现是有着不同的方法。
对于多线程而言通过让部分线程进行阻塞I/O或者非阻塞I/O加轮询技术来完成数据获取,让一个线程进行计算处理,通过线程之间的通信将I/O得到的数据进行传递,这就实现了异步I/O。

展开阅读全文

没有更多推荐了,返回首页