Nodejs 中的同步与异步、阻塞与非阻塞
最近在微信读书上读一本关于 Nginx 的书,书中提到了异步、同步、阻塞和非阻塞等概念。看到一些用户的笔记对这些概念进行了错误的解释,但却有很多人点赞。这让我不禁思考:现在连辨别错误信息的能力都没有了吗?
在学习 Node.js 的过程中,我们经常会听到同步/异步、阻塞/非阻塞这些术语。那么究竟什么是同步/异步?什么又是阻塞/非阻塞呢?
Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时环境,以其非阻塞 I/O 和事件驱动架构而闻名。虽然 JavaScript 的执行是单线程的,但 Node.js 通过 事件循环 机制,将耗时的 I/O 操作交给底层线程池处理。当操作完成后,系统会通知 Node.js 将合适的回调函数添加到轮询队列中等待执行。
关于 阻塞,Node.js 官方文档中给出了明确的定义:
阻塞 是指在 Node.js 程序中,其它 JavaScript 语句的执行,必须等待一个非 JavaScript 操作完成。这是因为当 阻塞 发生时,事件循环无法继续运行 JavaScript。
在 Node.js 中,JavaScript 由于执行 CPU 密集型操作,而不是等待一个非 JavaScript 操作(例如 I/O)而表现不佳,通常不被称为 阻塞。在 Node.js 标准库中使用 libuv 的同步方法是最常用的 阻塞 操作。原生模块中也有 阻塞 方法。
在 Node.js 标准库中的所有 I/O 方法都提供异步版本,非阻塞,并且接受回调函数。某些方法也有对应的 阻塞 版本,名字以
Sync结尾。
从定义中可以明显看出,非 JavaScript 操作指的是不在 ECMAScript 规范中的 API 操作。因此,像 for 循环、数值计算这类操作并不属于阻塞操作。而标准库中所有以Sync结尾的方法才是真正的非 JavaScript 操作,它们会阻塞主线程中 JavaScript 代码的执行。
在 Node.js 中,我们经常听到异步回调这个概念,那么究竟什么是异步回调呢?在 JavaScript 中,回调是通过执行传入的函数来实现的,即当操作执行完成后,通过回调函数来通知调用方。既然有异步回调,自然也有同步回调。
1 | |
同步指的是代码的执行顺序与编写顺序一致,而异步则是指代码的执行顺序与编写顺序不一致。不过,同步回调可以直接改写成下面这种形式,所以在实际开发中很少使用同步回调。
1 | |
下面我们来看一个异步回调的例子:
1 | |
其中readFile的第二个参数就是异步回调函数。虽然名为异步回调函数,但它仍然是在主线程中执行的,因为事件循环机制需要通过调用这个回调函数来通知主线程操作已完成。这也是为什么异步操作不会阻塞主线程的原因——实际的 I/O 操作在后台线程中执行,只有回调函数在主线程中执行。
非阻塞方法需要通过异步回调函数来通知主线程操作完成,而阻塞方法会直接阻塞 JavaScript 主线程的执行。需要注意的是,阻塞和非阻塞都属于非 JavaScript 操作,纯粹的 JavaScript 操作(如循环、计算等)不能被称为阻塞或非阻塞。
由于非阻塞操作需要通过异步回调函数来通知主线程,当一个操作流程需要执行一系列 I/O 和网络操作,并且这些操作需要严格按照特定顺序执行时,就容易出现”回调地狱”的问题。为了解决这个问题,开发者们为异步回调披上了Promise的外衣,实现了链式调用,将原本需要嵌套的异步回调通过then方法串联起来。但这种方式仍然不如同步代码那样直观易懂,因此人们又为Promise添加了语法糖——async和await,让异步代码看起来更像是同步代码。