深入理解--异步和非阻塞
异步和非阻塞的概念实际上已经出现了很长一段时间。但是异步真正开始流行起来,是因为AJAX技术逐渐成为主流的web开发技术。非阻塞的概念真正流行起来,是当java引入NIO,也可以称作非阻塞IO的API,开始走进主流的开发人员的视线,真正流行起来,也可以认为是node.js带来的。
同步 ,异步,阻塞,非阻塞这几个概念相互之间联系紧密,很难区分。很多程序员都不知道它们之间的具体的不同。本文就会详细讨论这个问题,希望能帮助读者更好的了解这几个概念
同步和阻塞
首先,我们先开始介绍与异步和非阻塞对立的两个概念:同步和阻塞
对于web开发者来说,理解同步的概念相对比较容易,因为HTTP协议就是一个同步的协议。web浏览器向服务器发送一个请求并且等待它的响应。收到响应之后,浏览器才可以继续向服务器发送下一个请求,并且等待响应,周而复始的重复这个过程。在发送下一个请求之前必须等待响应的到达才行,这就成为了HTTP协议的一个巨大的性能瓶颈,当然为了解决这个问题,后来就出现了异步的AJAX技术。
阻塞的概念相对也是比较容易理解的。我们通过Java中的InputStream类的read方法来介绍阻塞的概念,文档中是这样描述read方法的:
If no byte is available because the end of the stream has been reached, the value -1 is returned. This method blocks until input data is available, the end of the stream is detected, or an exception is thrown.
意思就是,如果已经到了流的末尾没有可读取的数据,那么就会返回-1。这个方法会一直阻塞,直到有可读取的数据,或者已经读到了流的末尾,或者抛出一个异常。
这个方法的调用会一直阻塞,因为他会一直等待直到输入的数据可以用来读取。这通常会造成性能的瓶颈,因为这个方法会阻塞,导致无法继续执行随后的操作。
异步和非阻塞
异步和非阻塞就是同步和阻塞的相反面。在直觉上,可能会感觉这两个概念会有一些类似,因为他们都可以允许你们的线程在等待结果或者返回的时候不需要挂起整个线程。但是他们又有不同,因为异步调用通常需要包括一个回调机制或者事件机制,去主动通知调用方此时响应的结果已经可用了。而非阻塞调用往往会先返回一个任意的结果,然后调用者会不定时的反复去尝试获取返回的结果,直到结果已经可用了。这里的区别就是一个主动通知和被动去询问。举个例子,你去音乐店买周杰伦的专辑,但老板告诉你,现在没货,你就回去了,等到货到了,准备好了,老板会主动打电话通知你,专辑已经到啦,快来买吧,这就是异步机制,是主动通知的。而非阻塞则是,老板不会主动通知你,而是你自己隔个一两天就去这家店主动问问,专辑到了么,直到有一次你询问的时候,终于发现专辑到了。非阻塞的概念常常用于I/O中,而异步的概念则相对应用的比较广泛。
特别的,异步I/O,意味着I/O操作是独立于当前的那个线程的操作而进行的。可以理解为,另外新开启了一个线程去执行I/O操作,当I/O操作完成之后会主动直接将结果返回。这里说的更详细一点就是,我们知道底层数据准备好之后,还要从内核区域拷贝到线程的缓冲区,非阻塞操作在这种意义上来说,又是同步的,因为非阻塞不会将这个拷贝数据的过程完成,而是当数据准备好了,告诉线程,你可以执行系统调用,将内核区域的数据拷贝到线程的缓冲区了,当然这个过程是同步,而且由于是系统调用,所以这个拷贝的过程也是阻塞的。而异步操作则不是,系统会开启一个线程当数据准备好了,这个线程还会完成这个从内核区将数据拷贝到线程缓冲区的过程,当数据拷贝完成了,才通知调用者,这时候调用者就直接可以用了。
我们在看一个更详细的异步I/O的例子:
我们假设同步I/O意味着发出一个I/O命令,然后一直等待,直到I/O操作完成。也就是说,你发出一个read命令,然后这个线程接下来的执行操作会一直等待,直到已经读到了内容。异步I/O则是你发出一个I/O命令,然后这个I/O不会立即完成。你可以先去执行接下来的程序。异步会实现一个接口,允许IO操作不阻塞当前的线程,而且当操作完成之后,会主动通知你操作已经完成。
Non-blocking 在这里有一个很好的解释: this StackOverflow answer:
This term is mostly used with IO. What this means is that when you make a system call, it will return immediately with whatever result it has without putting your thread to sleep (with high probability). For example non-blocking read/write calls return with whatever they can do and expect caller to execute the call again. try_lock for example is non-blocking call. It will lock only if lock can be acquired. Usual semantics for systems calls is blocking. read will wait until it has some data and put calling thread to sleep.
非阻塞I/O意味着当你发起一个系统调用的时候,他会立即返回一个结果,而不是将你的线程睡眠。非阻塞的读写操作,会收到一个立即的返回值,然后请求者会反复去重试,不断的去尝试,直到可以开始读写操作了。类似于忙等的状态,不断的测试,但是线程没有被阻塞。try_lock就是一个非阻塞的调用,他会尝试去获取锁,直到锁可以获取。通常来说,系统调用会进入内核,一般都是阻塞的,所以read操作往往是阻塞的,会等待可用数据,并且将线程休眠。
现在,我们应该对于异步和非阻塞的概念已经有所了解了。下面我们就举个现实中的例子来加强理解:
例如,传统的sockets API中,一个非阻塞的socket,通常会立即返回一个"would block" 的错误信息,然后需要调用独立的函数select or poll 去轮询检查什么时候可以再次尝试去读取。
但是异步的sockets (windows的sockets支持异步操作),.Net框架中也有异步I/O模型。你调用一个方法开始某个操作,然后 框架会在这个操作完成的时候,回调通知你,操作完成了。