Node.jsNode.js 中的线程 与并发
1. JavaScript 与线程
1.1 JavaScript 是单线程语言
是的——作为单线程语言 JavaScript 拓展包括 文件IO 在内的各种 API 的运行时 NodeJS,仍然是以单线程的模式运行的。
线程 是指同时运行多个任务或程序的执行。每个能够执行代码的单元称为线程。
1.2 浏览器中的线程
一般除非使用 web worker ,不然 JavaScript 只在线程中运行。浏览器中的线程包括 主线程 和 页面线程。
- 主线程用于浏览器处理用户事件和页面绘制等。
- 一般而言,浏览器在一个线程中运行一个页面中的所有 JavaScript 脚本,以及呈现布局,回流,和垃圾回收。
下面这张图来源于 https://www.google.com/googlebooks/chrome/,形象地描述了 Chromium 中的进程关系:
1.3 electron 中的线程
electron 是一个十分特殊的运行环境,它既拥有 NodeJS 运行时的API,也拥有浏览器的渲染界面。 Electron 框架在架构上非常相似于一个现代的网页浏览器,其的线程继承与 Chromium 中的线程,包括 主线程 和 渲染线程。
主进程
主进程的主要目的是 创建 和 管理 应用程序窗口,就行浏览器管理其页面那样。每个 Electron 应用都有一个单一的主进程,作为应用程序的入口点。 主进程在 Node.js 环境中运行,这意味着它具有 require 模块和使用所有 Node.js API 的能力。
渲染器进程
渲染器进程 简称 渲染进程。 每个 Electron 应用都会为每个打开的窗口生成一个单独的渲染器进程,这和浏览器中的页面进程(线程)是类似的。 洽如其名,渲染器负责 渲染 网页内容。 所以实际上,运行于渲染器进程中的代码是须遵照网页标准的 。
因此,一个浏览器窗口中的所有的用户界面和应用功能,都应与您在网页开发上使用相同的工具和规范来进行攥写。
1.4 创建额外线程
在早期,浏览器通常使用单个进程来处理所有这些功能。 虽然这种模式意味着您打开每个标签页的开销较少,但也同时意味着一个网站的崩溃或无响应会影响到整个浏览器。随着 JavaScript 技术发展至今,我们已经可以创建独立执行,同时能相互通信的额外进程。这得益于 Web Workers 相关技术。通过使用 Web Workers,Web 应用程序可以在独立于主线程的后台线程中,运行一个脚本操作。这样做的好处是可以在独立线程中执行费时的处理任务,从而允许主线程(通常是 UI 线程)不会因此被阻塞/放慢。
2. 事件循环 与异步消息执行机制
JavaScript 中,事件循环机制 负责执行代码、收集和处理事件以及执行队列中的子任务。之所以称之为 事件循环,是因为它经常按照类似如下的方式来被实现:
while (queue.waitForMessage()) { queue.processNextMessage(); }
一个 JavaScript 运行时包含了一个待处理消息的消息队列。每一个消息都关联着一个用以处理这个消息的 回调函数。
事件 与 消息的产生
每当一个事件发生 并且有一个 事件监听 器绑定在该事件上时,对应的一个消息就会被添加进 消息队列。如果没有事件监听器,这个事件将会丢失。
队列中消息的处理
而对 于在消息处理队列,在 事件循环 期间的某个时刻,运行时会从最先进入队列的 消息 开始处理队列中的消息。被处理的消息会被移出队列,并作为输入参数来调用与之关联的函数。调用一个函数总是会为其创造一个新的栈帧。
例如,当一个带有点击事件处理器的元素被点击时,产生了一个点击的消息加入到消息队列,直到后续处理到该消息时, 运行时会将该消息移出消息队列,然后执行这个消息的某些定义的回调函数,即所谓 关联的函数。这些关联函数可以通过相关API指定,比如对于点击事件:
const elem = document.getElementById("myBt"); elem.addEventListener("click", (event)=>{ // 关联函数(该点击事件的回调函数) })
添加消息
出了如 web 中的事件能够发出消息外,我们也可以手动地往消息队列中添加消息。例如我们可以使用一个 setTimeout
函数往消息队列中 添加异步消息。
// 注: // 浏览器中,window.setTimeout 和 setTimeout 是一回事。setTimeout中的this指向 Window 对象 // nodeJS中,setTimeout中的this 指向 Timeout 对象 window.setTimeout(()=>{ console.log("Hello, setTimeout!") }, 2000);
该函数接受两个参数:
- 待加入队列的消息;
这个时间值代表了消息被实际加入到队列的最小延迟时间。
如果队列中没有其他消息并且栈为空,在这段延迟时间过去之后,消息会被马上处理。
但是,如果有其他消息,setTimeout 消息必须等待其他消息处理完。 - 一个时间值。
需要指出的是,从以上讨论我们可知。这个参数仅仅表示最少延迟时间,而非确切的等待时间。
由于 setTimeout
添加的消息是异步的。异步消息只有在运行时执行完消息处理队列中的同步消息后才会被处理,处理时相应执行其关联函数。
执行栈 与 同步任务的执行
执行栈 是一个表示同步任务执行顺序的栈,主线程只执行来自执行栈的同步任务,被执行完成的任务将会出栈,直到 执行栈被清空。此过程中不会理会任何异步任务。
异步任务的执行
即使有相应的消息表明某些事件的发生,异步任务在执行栈未清空时都将被挂起。然而一旦执行栈清空,添加到消息队列中的消息按照之前所介绍的逻辑逐个出队,并执行对应的事件处理程序。直到消息队列中所有的消息均被处理完成。
概括
- JavaScript 运行时通过 单线程循环 处理事件;
- 所谓 事件,其实就是表示 消息被处理;
- 所谓 (事件的)回调函数,其实就是消息关联的函数;也就是 当消息被处理时 所执行的该 消息所绑定的函数,也称 事件处理程序;
- 所谓 任务,无非是 处理消息、执行消息绑定的函数;
- 任务有两种类型,即所谓 同步任务 和 异步任务;
- 运行时 先依次对 执行栈 中的所有 同步任务 出栈并执行,直到将其清空;
- 执行栈清空后,消息队列 中的异步消息逐个出队,交由 运行时处理以完成他们的事件处理程序,直到 消息队列清空。
3. Node.js 并发
3.1 概述
既然 NodeJS 是单线程的,那么怎么还可以实现并发呢?其实 NodeJS 可以通过创建多个子进程来实现,这一切都需要用到一个特殊的模块:child_process
。
child_process 模块来创建子进程的主要方法有如下:
3.2 exec
该函数的语法格式如下:
异步进程创建 API
child_process.exec(command[, options][, callback])
参数:
- command 要运行的命令,参数以空格分隔。
- options选项参数:
- cwd
<string> | <URL>
子进程的当前工作目录。 - env 环境键值对。默认为
process.env
- encoding 默认为
'utf8'
- shell 用来执行命令的Shell,Unix 上默认为
'/bin/sh'
,windows上默认为process.env.ComSpec
。 - signal 允许使用AbortSignal中止子进程。
- timeout 默认为 0。
- maxBuffer stdout或stderr上允许的最大数据量(以字节为单位)。如果超过,子进程将被终止,任何输出都将被截断。默认为
1024 * 1024
- killSignal
<string> | <integer>
默认为'SIGTERM'
- uid
<number>
设置进程的用户标识 - gid
<number>
设置进程的组标识 - windowsHide
<boolean>
隐藏通常在Windows系统上创建的子进程控制台窗口。
- callback当进程终止时用输出调用的回调函数。
- error
<Error>
- stdout
<string> | <Buffer>
- stderr
<string> | <Buffer>
返回:
<ChildProcess>
同步进程创建 API
child_process.execSync(command[, options])
参数:
- command 要运行的命令。
- options 可选参数,参考异步API。
返回:
<Buffer> | <string>
命令的标准输出。
3.3 execFile
child_process.execFile() 函数类似于child_process.exec(),只是它在默认情况下不生成shell。相反,指定的可执行文件作为一个新进程直接生成,比child_process.exec()更有效。
异步进程创建 API
child_process.execFile(file[, args][, options][, callback])
同步进程创建 API
child_process.execFileSync(file[, args][, options])
3.4 spawn
该函数的语法格式如下:
异步进程创建 API
child_process.spawn(command[, args][, options])
同步进程创建 API
child_process.execSync(command[, options])
3.5 fork
该函数的语法格式如下
child_process.fork(modulePath[, args][, options])