再聊聊阻塞/非阻塞&&同步/异步

说起阻塞/非阻塞我们就会想到同步/异步。确实,这两组概念可以算得上是一对老生常谈的老冤家了。百度一下同步/异步,结果里面一定会出现阻塞/非阻塞的身影,反之亦然。然而,就算有这么多的讨论、文章,但或许是汉语言带来的歧义性,能准确定义并分清这两组概念的人却不多。

首先我们来看看block/non-block一般的定义:

block/non-block的概念一般用于IO。block表现为进程调用外部操作(如IO)时,进程会挂起,直到外部调用返回才会继续执行。如果外部操作的调用是non-block,那么进程执行non-block操作时,该外部操作会立即返回,进程继续执行。对于non-block的操作,我们的程序需要提供一种方式,去跟进该外部操作的结果。比如通过定时轮询来查看外部操作的返回值。

这里说的很清楚:对于进程来说,IO操作属于外部调用,如果IO调用是阻塞的,那么当我们的进程执行IO操作时,它会等待IO操作完成才会接着执行后续的操作,此时进程会被挂起,CPU将不再执行当前进程相关的操作。

我们提到了进程挂起,那么,同步是否等于进程挂起?

所谓进程挂起,就意味着我们的进程被短暂的移出了内存,CPU不再执行该进程相关的操作。而对于同步的函数调用来说,只要不涉及外部操作,当前进程一定会执行同步函数内的内容,因此,进程并没有挂起。从主观上你会觉得函数一直在等待另一个函数返回,但如果将调用栈展开,我们会发现,等待意味着CPU繁忙而非空闲。

因此,对调用方来说阻塞IO意味着调用方会挂起等待;对非阻塞IO,调用方则需要提供一种机制来检查IO操作是否完成。

那么同步/异步呢?

异步是指函数调用时调用者不会立即得到返回结果。被调用者会通过回调、事件、消息等方式在合适的时间来通知调用者执行后续的操作。例如,我们熟悉的订阅者模式就是实现异步调用的一种方式。

因此,我们可以说,异步和同步是一种消息通知的机制。在顶层应用开发者眼中,异步往往就意味着无阻塞,因此这两个概念常常被混淆,但实际上它们并不一样。

确实绝大多数时候,异步调用意味着我们的主线程不会被阻塞。但异步并不一定就意味着我们的I/O是无阻塞的,我们可以通过新增线程防止阻塞I/O阻塞我们的主线程。但对于单线程的模型来说,阻塞I/O一定不能实现异步调用。比如我们熟悉的NodeJS:

NodeJS首页用黑体字写着:

NodeJS使用了一个事件驱动、非阻塞式的I/O模型,使其轻量又高效。

之前看到这里还有点疑惑:因为从应用开发者的角度,NodeJS为我们提供的是一组异步I/O的API,而即使是阻塞I/O,我们也可以通过轮询、回调等方式实现异步调用,从而避免主线程的阻塞。

但由于NodeJS不存在多线程的概念,如果它的I/O是阻塞IO,那么无论采取什么措施,它一定会阻塞主线程,而我们又不能通过增加线程来避免对主线程的阻塞。因此NodeJS的无阻塞I/O是真正意义上的无阻塞I/O。

那么问题又来了:

得益于底层的无阻塞API,NodeJS可以将I/O操作封装为异步方法。但对运行在浏览器中的JS来说,它所面对的网络I/O并不是无阻塞的,它如何在单线程上实现ajax呢?

答案是多线程。前面不是说浏览器端的JS是单线程吗?事实上,单线程仅仅是说JS运行在浏览器中的某个单一线程上,但并没有说我们的浏览器只有一个线程啊。事实上,浏览器提供了单独的线程来处理事件轮询和网络请求。当我们发出一个ajax请求时,浏览器会将回调函数和对应的事件注册到event queue,并创建一个新线程来处理网络请求。当网络请求结束时,它会通知event queue,事件已经完成。当我们的event loop下一次处理event queue时,它就会触发已完成事件的回调函数。

最后再总结一下:

如果我们聊的是阻塞/无阻塞IO,那么意味着,我们的I/O调用会/不会阻塞调用它进程,也就是调用方不会因为调用了I/O操作而挂起。而如果我们说的是同步/异步调用(同步/异步往往针对的就是函数调用的方式),那么我们谈论的就是被调用的函数是否提供了一种消息通知的机制来告知调用者它的执行状态,从而延迟某些事件的执行。

作为顶层的应用开发者,我们可以模糊甚至忽略掉阻塞和非阻塞的概念,只需要知道如果我们调用的是封装了I/O操作的异步方法(在JS中表现为提供了回调机制),那么它一定不会影响当前线程的执行;如果我们调用了封装了I/O操作的同步方法,那么当前线程会等待I/O操作返回

延伸阅读:

知乎:怎样理解阻塞/非阻塞和同步/异步的区别

How does a single thread handle ajax in Javascript