完全理解同步/异步与阻塞/非阻塞

随便翻开一本 Node.js 入门书籍的绪论部分,一般都可以看到「异步」、「单线程」、「非阻塞」这样的字眼。因其采用异步非阻塞的模型而构建,Node.js 得以能充分利用 CPU 资源,具有极强的处理高并发请求的能力。

可是到底什么是同步和异步?什么是阻塞和非阻塞?同步就意味着阻塞吗?异步就一定是非阻塞吗?即便是业务经验十分丰富的 Node.js 程序员,都不一定对这些概念辨别得十分明晰。

本文力求以简明的语言来解释清楚这几个概念并加以区分,但不涉及到 Node.js 底层的具体实现。

阻塞和非阻塞

从简单的开始,我们以经典的读取文件的模型举例。(对操作系统而言,所有的输入输出设备都被抽象成文件。)

在发起读取文件的请求时,应用层会调用系统内核的 I/O 接口。

如果应用层调用的是阻塞型 I/O,那么在调用之后,应用层即刻被挂起,一直出于等待数据返回的状态,直到系统内核从磁盘读取完数据并返回给应用层,应用层才用获得的数据进行接下来的其他操作。

如果应用层调用的是非阻塞 I/O,那么调用后,系统内核会立即返回(虽然还没有文件内容的数据),应用层并不会被挂起,它可以做其他任意它想做的操作。(至于文件内容数据如何返回给应用层,这已经超出了阻塞和非阻塞的辨别范畴。)

这便是(脱离同步和异步来说之后)阻塞和非阻塞的区别。总结来说,是否是阻塞还是非阻塞,关注的是接口调用(发出请求)后等待数据返回时的状态。被挂起无法执行其他操作的则是阻塞型的,可以被立即「抽离」去完成其他「任务」的则是非阻塞型的。

阻塞和非阻塞调用模型

同步和异步

阻塞和非阻塞解决了应用层等待数据返回时的状态问题,那系统内核获取到的数据到底如何返回给应用层呢?这里不同类型的操作便体现的是同步和异步的区别。

对于同步型的调用,应用层需要自己去向系统内核问询,如果数据还未读取完毕,那此时读取文件的任务还未完成,应用层根据其阻塞和非阻塞的划分,或挂起或去做其他事情(所以同步和异步并不决定其等待数据返回时的状态);如果数据已经读取完毕,那此时系统内核将数据返回给应用层,应用层即可以用取得的数据做其他相关的事情。

而对于异步型的调用,应用层无需主动向系统内核问询,在系统内核读取完文件数据之后,会主动通知应用层数据已经读取完毕,此时应用层即可以接收系统内核返回过来的数据,再做其他事情。

这便是(脱离阻塞和非阻塞来说之后)同步和异步的区别。也就是说,是否是同步还是异步,关注的是任务完成时消息通知的方式。由调用方盲目主动问询的方式是同步调用,由被调用方主动通知调用方任务已完成的方式是异步调用。

同步和异步调用模型

Node.js 的异步非阻塞模型

完整来说,一个最高效且理想的文件读取异步非阻塞模型应该是这样的:应用层发起调用后系统内核立即返回(还没有文件内容数据),应用层继续做其他无关的事情,在系统内核从磁盘读取完数据之后主动通知应用层任务已完成,应用层此时接收系统内核返回的数据,然后继续做其他相关或不相关的事情。

可以看到,在这个模型中,没有无谓的挂起、休眠与等待,也没有盲目无知的问询与检查,应用层做到不等候片刻的最大化利用自身的资源,系统内核也十分「善解人意」的在完成任务后主动通知应用层来接收任务成果。

Node.js 是不是就是这样实现的呢?是,也不是。

现实总是比理想骨感,系统内核并没有理想中那样「善解人意」。异步模型的内核调用在各个平台上实现不一,而且各有各的问题,所以实际上, Node.js 其实是借助多线程来模拟实现了上述理想的异步非阻塞模型。

有人可能有疑问,前面不是说 Node.js 是单线程的吗?

实际上单线程是对用户(使用 Node.js 进行上层开发的程序员,而不是开发 Node.js 的人员)而言的。Node.js 在底层对多个 I/O 操作是借助多线程实现异步非阻塞的,具体来说,Node.js 总是存在一个主线程,用来管理调度 I/O 线程并进行运算,而其他的线程都是 I/O 线程。I/O 线程在主线程的调度下与系统内核进行交互完成完成 I/O 操作并把数据返回给主线程,而主线程对 I/O 线程的调度就完全是上述异步非阻塞的(至于 I/O 线程是异步还是同步、阻塞还是非阻塞,已经不重要了,因为它不影响主线程的效率,只要它能按时返回预期的数据就行)。我们平时所说的 Node.js 是单线程的,就是指 Node.js 的主线程。I/O 线程完全是对用户屏蔽的,所以用户根本无需关心。

这也解释了为什么我们要避免书写计算密集型或者阻塞的代码,一旦主线程被阻塞,那整个应用就是真的都被阻塞了。

场景举例与总结

最后,再来举一个我们日常的例子来加深对这几个概念的理解。

假设小明需要在网上下载一个软件:

  • 如果小明点击下载按钮之后,就一直干瞪着进度条不做其他任何事情直到软件下载完成,这是同步阻塞;
  • 如果小明点击下载按钮之后,就一直干瞪着进度条不做其他任何事情直到软件下载完成,但是软件下载完成其实是会「叮」的一声通知的(但小明依然那样干等着),这是异步阻塞;(不常见)
  • 如果小明点击下载按钮之后,就去做其他事情了,不过他总需要时不时瞄一眼屏幕看软件是不是下载完成了,这是同步非阻塞;
  • 如果小明点击下载按钮之后,就去做其他事情了,软件下载完之后「叮」的一声通知小明,小明再回来继续处理下载完的软件,这是异步非阻塞。

相信看完以上这个案例之后,这几个概念已经能够分辨得很清楚了。

总的来说,同步和异步关注的是任务完成消息通知的机制,而阻塞和非阻塞关注的是等待任务完成时请求者的状态

References

  1. 《深入浅出 Node.js》第三章;
  2. 聊聊同步、异步、阻塞与非阻塞