大家好,我是徐徐。今天我们讲讲如何在 Electorn 中进行多线程操作,先来认识一下 worker_threads 。
前言
我们在开发一个应用的时候可能会遇到一些比较特殊的场景,比如数据的大量计算, 图片处理和视频编码 , 密集型加密/解密操作等一些 CPU 密集型任务,这些非常规的动作可能会阻塞整个程序,使得整个应用变得卡顿。为了提升应用的响应速度和用户体验,我们需要将这些耗时操作放到线程中去执行,从而避免阻塞整个进程。下面我们就来看看如何在 Electorn 中进行多线程的操作吧。
Worker Threads 基础
worker_threads 模块允许使用并行执行 JavaScript 的线程,工作线程对于执行 CPU 密集型的 JavaScript 操作很有用。Worker 对 I/O 密集型的工作帮助不大,Node.js 内置的异步 I/O 操作比工作线程更高效。与 child_process 或 cluster 不同,worker_threads 可以共享内存,它们通过传输 ArrayBuffer 实例或共享 SharedArrayBuffer 实例来实现。
用一段代码看看 worker_threads
javascript
// 从 worker_threads 模块引入 Worker 类
improt { Worker } from "worker_threads";
// 创建一个新的工作线程
// path: 工作线程脚本的路径
// data: 传递给工作线程的数据
const worker = new Worker(path, { data });
// 监听工作线程发送的消息
worker.on('message', msg => {
// msg 是工作线程通过 parentPort.postMessage() 发送的数据
});
// 监听工作线程中的错误
worker.on('error', err => {
// 处理工作线程中抛出的错误
});
// 监听工作线程结束事件
worker.on('exit', exitcode => {
// exitcode: 0 表示正常退出,非 0 表示异常退出
});
// 监听工作线程开始运行事件
worker.on('online', () => {
// 当工作线程开始执行时触发
});
这段代码展示了工作线程的四个主要事件
message
:接收工作线程传来的数据error
:处理工作线程的错误exit
:处理工作线程的退出online
:工作线程启动就绪
通信
如果此线程是 Worker,则这是允许与父进程通信的 MessagePort。使用 parentPort.postMessage() 发送的消息在使用 worker.on('message') 的父进程中可用,使用 worker.postMessage() 从父进程发送的消息在使用 parentPort.on('message') 的该线程中可用。
javascript
import { Worker, isMainThread, parentPort } from "worker_threads";
if (isMainThread) {
const worker = new Worker(__filename);
worker.once('message', (message) => {
console.log(message); // 打印 'Hello, world!'.
});
worker.postMessage('Hello, world!');
} else {
// 收到消息之后发送回去
parentPort.once('message', (message) => {
parentPort.postMessage(message);
});
}
实际应用例子
上面讲到的基本都是 Node.js 官网所写的,我们这里展示一个实际应用的例子来看看 Node 多线程的应用,这里必须要要拿出斐波那契数列这个例子了,因为它非常耗费 CPU,计算量很大,我们就拿它来做实验。
我们需要创建一个 worker 文件,这个 worker 文件里面就是你的各种处理逻辑,如下,这里我们的处理逻辑就是 getFibonacciNumber,当然你可以把这个方法换成其他的非常耗时、非常耗费 CPU 的函数。
- src/worker/fibonacci.work.js
javascript
import { parentPort, workerData,isMainThread } from "worker_threads";
if (isMainThread) {
console.log('Main thread');
} else {
parentPort.postMessage(getFibonacciNumber(workerData.num))
}
export function getFibonacciNumber (num) {
if (num === 0) {
return 0;
} else if (num === 1) {
return 1;
} else {
return getFibonacciNumber(num - 1) + getFibonacciNumber(num - 2);
}
}
有了 worker 脚本,我们需要在主进程中去调用,然后也把不调用线程的方法写在下面好做比较,不然怎么知道多线程有多香呢。
- src/main/worker/index.ts
javascript
import { Log4 } from "@/common/log";
import { Worker } from "worker_threads";
import { fileURLToPath } from 'url';
import { dirname, resolve } from 'path';
import {getFibonacciNumber } from "@/worker/fibonacci.work"
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
export const testFbonacciWorker = (number:Number) => {
console.time(`workerTime ${number}`);
const worker = new Worker(resolve(__dirname, '../worker/fibonacci.work.js'), { workerData: { num: number } });
worker.on("message", result => {
Log4.info (`${number}th Fibonacci Result: ${result}`);
console.timeEnd(`workerTime ${number}`)
});
worker.on("error", error => {
Log4.error(error);
});
worker.on("exit", exitCode => {
Log4.info(`结束 Code ${exitCode}`);
})
}
export const testMainThreadBlocking = () => {
console.time('blockingTest');
let sum = 0;
for (let i = 0; i < 10000000000000000000000; i++) {
sum += i;
}
console.timeEnd('blockingTest');
}
export const runFbonacciWorker = () => {
Log4.info('runFbonacciWorker start')
Log4.info('runFbonacciWorker 45')
testFbonacciWorker(45)
Log4.info('runFbonacciWorker 10')
testFbonacciWorker(10)
testMainThreadBlocking()
}
export const testGetFibonacciNumberWithoutWork = () => {
console.time('testGetFibonacciNumberWithoutWork')
Log4.info('testGetFibonacciNumber start')
const result1 = getFibonacciNumber(45)
Log4.info(`45th Fibonacci Result: ${result1}`)
const result2 = getFibonacciNumber(10)
Log4.info(`10th Fibonacci Result: ${result2}`)
Log4.info('testGetFibonacciNumber')
console.timeEnd('testGetFibonacciNumberWithoutWork')
testMainThreadBlocking()
}
在界面上写两个按钮分别触发一下两个方法,比较一下用了 worker_threads 和没用 worker_threads 的效果。
- 使用了 worker_threads ,主进程不会出现阻塞情况,异步回调机制。
- 没有使用 worker_threads ,主进程会出现阻塞情况,并且界面会出现严重卡顿,导致界面未响应。
结语
这里我们只是简单得认识了一下 Node 中的 worker_threads,它是 Node 多线程的基础,在较为复杂的应用中都可能会使用到。不过简单的使用可能还并不能满足一些特定需求,下一节我们将更加深入得讨论它,涉及线程池、内存共享、线程异常处理等知识体系,并结合实际场景做出相应的演示。