Node.js是单线程的还是多线程的?为什么?
你是否读过很多文章,试图弄明白 Node.js 究竟是单线程还是多线程?为什么有的文章说是单线程,有的说是多线程?我也有过同样的困惑,读了一篇又一篇的文章后,心中仍然充满疑惑,觉得这个概念仍然没有完全理解。本文旨在帮助你解开这个疑惑。
根据Node.js 文档,Node.js 应用程序使用事件循环运行。事件循环使得 Node.js 能够执行非阻塞 I/O 操作,并解释了 Node.js 如何实现异步执行。事件循环(也称为主线程)一次只能运行一个任务。也就是说,Node.js JavaScript 代码运行在单个线程上。
现在,你可能已经在其他文章中读到过一些要点,例如使用worker_threads来实现多线程,或者用于开发 Node.js 应用程序的编程语言使其变为单线程等等。我将介绍这些相关要点,但在继续之前,我将回顾一下单线程和多线程进程的概念。
什么是单线程进程
单线程进程是指按单一顺序执行程序指令。也就是说,如果一个应用程序有以下指令集:
- 说明 A
- 指令 B
- 指令 C
什么是多线程进程
多线程进程是指按多个顺序执行程序指令。因此,除非多个指令被分组到不同的序列中,否则指令无需等待即可执行。
为什么Node.js是单线程的?
现在你知道Node.js架构是单线程的。但是,为什么它是单线程的呢?我的第一个问题是,你理解事件循环的工作原理吗?如果不理解,我建议你阅读这篇文章。
为了简化操作,事件循环一次只运行一个进程。这意味着它一次只能执行一个函数,而由于函数可以包含多条指令,因此事件循环一次只能执行一条指令。
乍听之下,它似乎效率不高,性能欠佳。然而,事实恰恰相反,它的性能和可扩展性都优于其他多线程方案,例如 Java。
运行多线程解决方案需要利用系统的多个核心。然而,如果一个线程正在等待 I/O 响应,其他线程可能仍在运行。理论上,多线程似乎是最佳方案,但我们忽略了一点:即使其他线程可用,某个线程仍然可能被阻塞。
事件循环的妙处不在于将所有操作都放在单个线程中运行,而在于它可以“搁置”耗时的 I/O 操作,从而保证其他指令的执行。这就是为什么即使多个用户同时向 Node.js API 发送请求,我们也能获得快速响应的原因。
首先需要澄清的是,并不存在“同时发出多个请求”这种说法。表面上看起来好像同时运行了多个请求,但实际上,事件循环会根据请求到达的顺序,运行为每个请求定义的进程。为了便于理解,我们举个例子。假设我们有以下 API 端点:
- /getCars
- /更新车辆
- /更新驱动程序
请记住,请求并非同时发出。事件循环将按照以下顺序处理请求,假设这是请求的顺序:
- /getCars
- /更新车辆
- /更新驱动程序
事件循环会执行来自 `/getCars` 端点的第一条指令。之后,会有一个指令,即从 API 向数据库发出请求以获取车辆信息。这被视为一次 I/O 操作。此过程的执行时间可能很短,也可能很长。无论执行速度如何,事件循环都会触发此请求并将其“移出”,以避免阻塞线程执行其他指令。但是,一旦收到数据库的响应,事件循环就会恢复执行 `/getCars` 端点的指令集。
因此,当 /getCars 端点向数据库发出请求并等待响应时,/updateCar 端点会执行其指令集。如果 /updateCar 端点没有 I/O 操作,则会在 /getCars 端点返回响应之前返回响应。
类似地,如果 `/updateCar` 端点包含执行 I/O 操作的指令,事件循环会触发该指令,但不会阻塞线程执行其他指令。这样,线程既可以开始执行 `/updateDriver` 端点的指令集,也可以在收到数据库响应后恢复执行 `/getCars` 端点的指令集。这取决于事件队列中哪个事件先被添加进去。
仔细想想,Node.js 架构的主要优势并非在于其单线程特性,而在于它能够避免阻塞线程执行其他指令。这正是 Node.js 成为 API 开发理想选择的主要原因之一,因为 API 开发往往高度依赖 I/O 操作。事件循环机制能够智能地执行密集型 I/O 操作,并在 I/O 操作完成后恢复进程,而无需担心多线程解决方案可能带来的死锁或竞态条件等问题。因此,对于许多团队来说,使用 Node.js 是理所当然的选择。
不要阻塞事件循环(即主线程)
大多数解决方案都有其优缺点,Node.js 也不例外。我们知道 Node.js 使用事件循环(也就是主线程)运行,因此阻塞事件循环确实会阻止系统执行其他指令,无论这些指令属于单个进程还是多个不同的进程。
你不是说过事件循环“触发密集型操作并将其移到一边,一旦操作得到响应,就恢复该进程”吗?
是的。
然而,需要澄清的是,事件循环“恢复”I/O操作进程的能力并不意味着它可以绕过密集型CPU操作。I/O操作的优势在于利用外部CPU处理能力来执行进程。但是,如果我们的Node.js应用程序本身就占用了大量CPU处理能力来执行操作,那么在密集型指令执行完毕之前,我们就无法执行其他指令集。这被称为阻塞事件循环。
令人困惑的 JavaScript 和 Node.js 线程过程
不能因为 JavaScript 编程语言是单线程的就断言 Node.js 也是单线程的。这种说法并不正确。JavaScript 可以在不同的编程环境中运行,而 Node.js 是目前最流行的 JavaScript 环境之一。因此,认为 JavaScript 是单线程的是一种常见的误解。在讨论单线程或多线程时,我们应该关注的是编程环境的运行方式,而不是语言本身。
Node.js 中的工作线程怎么样?它能让 Node.js 成为多线程的吗?
虽然 v10.5.0 中实现了工作线程,允许使用线程并行执行 JavaScript,但 Node.js 事件循环架构是基于单线程的。
使用worker_threads创建多个线程的真正目的是生成多个共享内存的 V8 引擎。工作线程非常适合执行 CPU 密集型的 JavaScript 操作。这样可以将主线程的事件循环从 CPU 密集型进程中解放出来,使其能够专注于最适合的密集型 I/O 操作。
生成工作线程的开销并不会给 I/O 密集型任务带来积极影响,因为最终每个线程的机制都相同:每个线程一个事件循环,这与不使用工作线程并无区别。Node.js 内置的异步 I/O 操作比工作线程更高效。
也就是说,每个线程都将使用相同的 Node.js 架构,而该架构本身是单线程的。您可以通过生成多个节点或 Node.js V8 引擎来实现多线程,但这些节点或引擎本身都是单线程的。因此,说 Node.js 本身不是多线程的仍然是正确的。
文章来源:https://dev.to/arealesramirez/is-node-js-single-threaded-or-multi-threaded-and-why-ab1
