Node.js 中的线程 与 并发

简介: Node.js 中的线程 与 并发

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 模块来创建子进程的主要方法有如下:

方法 描述
exec 生成一个shell,然后在该 shell 中执行命令,缓冲子进程的生成的输出。传递给exec函数的命令字符串由shell直接处理。
execFile 与 exec 类似,只是它在默认情况下不生成shell
spawn 使用给定的命令行参数生成一个新的进程,命令行参数在args中。如果省略,args默认为空数组。
fork 方法是 spawn() 的特例,专门用于生成新的Node.js进程。返回一个ChildProcess对象。返回的ChildProcess将有一个额外的内置通信通道,允许消息在父进程和子进程之间来回传递。

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])
目录
相关文章
|
19天前
|
并行计算 Java 数据处理
SpringBoot高级并发实践:自定义线程池与@Async异步调用深度解析
SpringBoot高级并发实践:自定义线程池与@Async异步调用深度解析
98 0
|
4天前
|
安全
List并发线程安全问题
【10月更文挑战第21天】`List` 并发线程安全问题是多线程编程中一个非常重要的问题,需要我们认真对待和处理。只有通过不断地学习和实践,我们才能更好地掌握多线程编程的技巧和方法,提高程序的性能和稳定性。
|
3月前
|
Java 开发者
解锁并发编程新姿势!深度揭秘AQS独占锁&ReentrantLock重入锁奥秘,Condition条件变量让你玩转线程协作,秒变并发大神!
【8月更文挑战第4天】AQS是Java并发编程的核心框架,为锁和同步器提供基础结构。ReentrantLock基于AQS实现可重入互斥锁,比`synchronized`更灵活,支持可中断锁获取及超时控制。通过维护计数器实现锁的重入性。Condition接口允许ReentrantLock创建多个条件变量,支持细粒度线程协作,超越了传统`wait`/`notify`机制,助力开发者构建高效可靠的并发应用。
86 0
|
17天前
|
Java
【编程进阶知识】揭秘Java多线程:并发与顺序编程的奥秘
本文介绍了Java多线程编程的基础,通过对比顺序执行和并发执行的方式,展示了如何使用`run`方法和`start`方法来控制线程的执行模式。文章通过具体示例详细解析了两者的异同及应用场景,帮助读者更好地理解和运用多线程技术。
22 1
|
2月前
|
网络协议 C语言
C语言 网络编程(十四)并发的TCP服务端-以线程完成功能
这段代码实现了一个基于TCP协议的多线程服务器和客户端程序,服务器端通过为每个客户端创建独立的线程来处理并发请求,解决了粘包问题并支持不定长数据传输。服务器监听在IP地址`172.17.140.183`的`8080`端口上,接收客户端发来的数据,并将接收到的消息添加“-回传”后返回给客户端。客户端则可以循环输入并发送数据,同时接收服务器回传的信息。当输入“exit”时,客户端会结束与服务器的通信并关闭连接。
|
2月前
|
数据采集 消息中间件 并行计算
进程、线程与协程:并发执行的三种重要概念与应用
进程、线程与协程:并发执行的三种重要概念与应用
51 0
|
3月前
|
算法 Java
JUC(1)线程和进程、并发和并行、线程的状态、lock锁、生产者和消费者问题
该博客文章综合介绍了Java并发编程的基础知识,包括线程与进程的区别、并发与并行的概念、线程的生命周期状态、`sleep`与`wait`方法的差异、`Lock`接口及其实现类与`synchronized`关键字的对比,以及生产者和消费者问题的解决方案和使用`Condition`对象替代`synchronized`关键字的方法。
JUC(1)线程和进程、并发和并行、线程的状态、lock锁、生产者和消费者问题
|
2月前
|
C语言
C语言 网络编程(九)并发的UDP服务端 以线程完成功能
这是一个基于UDP协议的客户端和服务端程序,其中服务端采用多线程并发处理客户端请求。客户端通过UDP向服务端发送登录请求,并根据登录结果与服务端的新子线程进行后续交互。服务端在主线程中接收客户端请求并创建新线程处理登录验证及后续通信,子线程创建新的套接字并与客户端进行数据交换。该程序展示了如何利用线程和UDP实现简单的并发服务器架构。
|
3月前
|
Rust 并行计算 安全
揭秘Rust并发奇技!线程与消息传递背后的秘密,让程序性能飙升的终极奥义!
【8月更文挑战第31天】Rust 以其安全性和高性能著称,其并发模型在现代软件开发中至关重要。通过 `std::thread` 模块,Rust 支持高效的线程管理和数据共享,同时确保内存和线程安全。本文探讨 Rust 的线程与消息传递机制,并通过示例代码展示其应用。例如,使用 `Mutex` 实现线程同步,通过通道(channel)实现线程间安全通信。Rust 的并发模型结合了线程和消息传递的优势,确保了高效且安全的并行执行,适用于高性能和高并发场景。
47 0
|
3月前
|
Java 开发者
【编程高手必备】Java多线程编程实战揭秘:解锁高效并发的秘密武器!
【8月更文挑战第22天】Java多线程编程是提升软件性能的关键技术,可通过继承`Thread`类或实现`Runnable`接口创建线程。为确保数据一致性,可采用`synchronized`关键字或`ReentrantLock`进行线程同步。此外,利用`wait()`和`notify()`方法实现线程间通信。预防死锁策略包括避免嵌套锁定、固定锁顺序及设置获取锁的超时。掌握这些技巧能有效增强程序的并发处理能力。
25 2