nodejs之worker_threads学习记录

1,143 阅读9分钟

nodejs之worker_threads学习记录

文档创建人创建日期文档内容更新时间
adsionli2022-07-26node.jsworker_threads2022-07-26

在开发可视化PPT的这个大功能的时候,突然想到缓存的使用,就选择使用了service-worker,然后又想到可以自定义缓存策略,就想到可以通过后端判断是否发生了更新,但是如何加快判断的过程呢,就想着可以做一个多线程的更新判断。然后就看到了node中的worker_threads模块了,既然需要使用这个模块,那就先来学习一下,理清楚一下其中的内容,也方便之后的使用嘞。

worker_threads

worker_threads在node主要就是用来进行多线程管理的一个库,是用来处理密集型CPU操作的一种很好地辅助手段,当然对于处理I/O来说的话,它的效率就不是特别好了。

worker_threads模块中主要包括了其本身的一些内容方法,以及WorkerMessageChannel这些类,接下来就一块一块内容梳理并记录,方便自己之后进行使用。

内置方法

isMainThread

isMainThread方法是用来判断当前线程是否是主线程的。可以在子线程中进行调用。

import { isMainThread } from "worker_threads"
//NOTE: 用来判断当前线程是否是主线程
if(isMainThread) {
    
}else {
    
}

上面就是使用的一个🌰,它的返回值是一个Boolean

parentPort

parentPort是一个很有用的方法,它可以让我们子线程与主线程进行通信的一个类,我们可以通过parentPort接受来自主线程的消息,也可以通过parentPortpostMessage函数,传递消息给主线程。它也是主要工作在子线程中的,下面是一个使用例子

const { Worker, isMainThread, parentPort } = require('worker_threads');

if (isMainThread) {
    const worker = new Worker(__filename);
    worker.once('message', (message) => {
        console.log(message);  // Prints 'Hello, world!'.
    });
    worker.postMessage('Hello, world!');
} else {
    //NOTE: 当消息从主线程发出时,我们可以通过parentPort.once监听message事件来获取,然后通过postMessage来回传消息
    parentPort.once('message', (message) => {
        parentPort.postMessage(message);
    });
}

resourceLimits

resourceLimit是用来限制子线程可以使用的资源数量的,其主要的资源设置有以下几个

  • maxOldGenerationSizeMb:子线程中栈的最大内存

  • maxYoungGenerationSizeMb:子线程中创建对象的堆的最大内存

  • codeRangeSizeMb:生成代码消耗的内存

  • stackSizeMb:该线程默认堆的大小

其主要作用在子线程创建时候的,声明其内存资源分配限制

workerData

workerData主要用于子线程获取主线程创建时,传递给子线程的数据的。

workerData的设置也是在子线程创建的时候进行设置,其设置和使用如下:

const { Worker, isMainThread, workerData } = require('worker_threads');

if (isMainThread) {
    const worker = new Worker(__filename, { workerData: 'Hello, world!' });
} else {
    console.log(workerData); 
}

markAsUntransferable

markAsUntransferable函数的作用是将一个数据设置为非transfer传递对象,具体作用在下面和transferList一起说明,单独说的话不太好理解。

moveMessagePortToContext

说实话,一开始没看懂这个函数,感觉怪怪的......后面查了一些别人的,大概知道它是干啥的了。

moveMessagePortToContext的作用就是将MessageChannel这个原本用来线程间通信的类,切换一个独立的、自定义的context对象,而不是默认的上下文对象了。

它主要接收两个参数,一个是原来通过MessageChannel创建的port实例对象,还有一个参数就是自定义的context上下文对象,然后它的返回就是修改了上下文对象之后的port对象。

不过在我写的时候,没怎么用到这个,就不管啦。

receiveMessageOnPort

receiveMessageOnPort函数能够获取到指定port(MessageChannel)的最后一条信息。

在写的时候也没怎么使用到,但是还是记录一下,以防之后进行使用

SHARE_ENV

SHARE_ENV这个参数可以在new Worker时,作为options中的env的参数传入,代表子线程也可以获取到主线程设置process.env属性,也就是全局环境变量。就和使用vue的时候设置的那个一样的,这里就是为了让子线程可以获取到。

get/setEnvironmentData

setEnvironmentDatagetEnvironmentData作用的地方不同。

  1. setEnvironmentData被使用在主线程中,在主线程中可以通过setEnvironmentData来设置环境数据内容,它的参数是key,value,这里的key可以设置为可用作Map键的任意、可克隆的值,value就是需要传递的内容。
  2. getEnvironmentData被使用在子线程中,用来获取主线程中设置的EnvironmentData,通过key进行获取。

示例代码如下:

const {
    Worker,
    isMainThread,
    setEnvironmentData,
    getEnvironmentData,
} = require('node:worker_threads');

if (isMainThread) {
    setEnvironmentData('Hello', 'World!');
    const worker = new Worker(__filename);
} else {
    console.log(getEnvironmentData('Hello'));  // Prints 'World!'.
}

注:使用这两个函数的node版本存在一定的限制,node版本需要>= 14.18.0

好了,差不多了,基本把文档中涉及到的内容都梳理完了(我的node设置的版本是14.18.0),这里是根据自己使用的Node版本来的。

MessageChannel类

主要内容说明与参数如下:

  1. MessageChannel类的主要作用是用来进行子线程之间的通信的。
  2. MessageChannel只能够支持两两线程通信,不能大规模通信。

MessageChannel也是继承了EventEmit

MessageChannel类里面的方法比较简单,就不展开说了,就用列表记录一下

名称类型作用
close函数用来关闭通信(双向关闭),一旦调用,信道就会关闭,无法postMessage
ref函数用于激活挂起的信道,即激活unref之后的信道,如果已经是激活状态的了,就不会在执行了
unref函数用于挂起信道,但是不删除,就是退出了通信,可以用ref进行激活
start函数start的作用就是在如果没有设置onmessage监听函数的时候,用于忽略消息的,如果设置了onMessage,那么onmessage中会自动调用start函数进行消息跳过。如果不设置start函数,消息就会进行排队,也就是说start是用来消费传送来的消息的
postmessage函数用来发信的,这里不展开说了,在下面worker里面具体说这个函数,因为其实现是一样的。
close监听事件监听当前通信通道关闭事件
message监听事件监听通信通道发信事件
messageerror监听事件监听通信信道发信失败事件

Worker类

关于Worker,在使用的时候,感觉有一些内容可以分享给大家:

  1. Worker类的作用就是创建一个子线程,可以通过new Worker进行创建,创建的同时必须传一个filePath或者是一个string类型的函数(需要在options中设置evaltrue才可以),来指定其执行内容。
  2. Worker类继承了EventEmit类,所以可以使用EventEmit的相关函数及参数。
  3. Worker子线程可以通过创建MessageChannel来进行子线程之间的通信,当然这个需要在主线程中进行控制,也可以是子线程中创建后,使用transferList,传递给主线程并指定需要通信的子线程,再有主线程进行分发到指定线程。
  4. Worker子线程之间可以使用SharedArrayBuffer这个共享内存进行数据同步,任意一个子线程都可以对SharedArrayBuffer进行使用与修改。

threadId

可以通过threadId获取当前子线程的线程标识

once与on的事件捕获

once只执行一次,之后就不会在进行监听,但是on不同,on可以持续的进行监听,每一次都会发出响应。

postMessage

从主线程向子线程传递消息,其中包括两个参数,一个是正常的value,还有一个可选参数transferList

value注意项
  1. value可以设置循环引用、Js类型实例、类型化数组、Wasm等
  2. value不可设置为:
    1. FileHandle
    2. Histograme
    3. KeyObject类(加密秘钥)
    4. MessagePort类(可以在transferList中使用)
    5. net.BlockList
    6. net.SocketAddress

上面是文档中给出的

transferList解释与注意项

transferList是一个listlist中的对象可以是ArrayBuffer、MessagePort、FileHandle

如果value中包含SharedArrayBuffer对象,那么该对象不能被包含在transferList中。

几个重要的点:

1. shaderdArrayBuffertransferList不能同时使用。

2. 在postMessage中,如果指定了transferList的数据,那么在当前线程中之后的使用中,transferList中的数据就无法被使用了,因为它会向我们投递物品之后,我们没有了这个物品一样,无法继续使用

3. 如果通过markAsUntransferable设置过的buffer变量,那么仍然是可以被之后使用的,因为markAsUntransferable会将数据进行标记,变为不可transferable

unref/ref

线程挂起与恢复,这个主要是用来在主线程中,控制子线程状态进行使用的。unref与ref的作用就和MessageChannel的作用是一样的,我们可以使用unrefref完成线程的复用,不过这就和之后需要实现的内容有关系了,我使用了unref/ref作为线程在有效时间内的挂起和激活。

unref/ref的使用也是在主线程中进行使用的。

unref/ref的使用,不会触发exitonline事件

terminate

线程关闭,也就是直接销毁一个子线程对象,也是在主线程中进行使用的。

terminate可以触发exit事件

stdin/stderr/stdout

这三个函数没怎么用到,也有点不太知道是干啥的,网上查了一下,也没什么人说,估计确实用的比较少。不过看文档里面,大概猜测一下,应该是可以通过这三个函数,来获取到在主线程的输入流,输出流和流读取的,不过没用过也不确定对不对,也不知道可以用在什么地方,知道的xdm可以评论告诉我一下😂。

这三个函数如果需要使用的话,还需要在new Worker时,配置一下options中的stdin: true, stdout: true, stderr: true才可以使用。

performance

performance参数可以用来获取当前线程的性能信息的对象,具体有些啥我没怎么用过不太知道,等到后面使用了回来补全。

结语

本片文章主要是为了整理一下Node官网文档中worker_threads模块,里面有些语句描述实在是不太看得懂,所以记录一下,方便之后自己继续开发😂。接下来就是根据这篇文章中的知识点,来写一个还算完整的线程池,给自己使用,嘿嘿

在node的18.x版本中,还新增了BoradcastChannel类,它可以用来1对多进行通信。这里提一下