• 如何使用Node.js实现多线程的应用
  • 发布于 2个月前
  • 309 热度
    0 评论
  • 柠檬酸
  • 13 粉丝 40 篇博客
  •   
前言

最初,JavaScript是用于设计执行简单的web任务的,比如表单验证。直到2009年,Node.js的创建者Ryan Dahl让开发人员认识到了通过JavaScript 进行后端开发已成为可能,在后端开发中,用到最多的就是多线程以及线程之间的同步功能,今天小编就为大家介绍一下如何使用Node.js实现多线程的应用。


Node.js的内部工作原理

在介绍之前,先给大家介绍一下Node.js的工作原理,Node.js基于单线程事件循环的范例进行操作。为了充分掌握Node.js的功能,理解Node中线程(构成Node.js核心的事件循环)的概念至关重要。


Node.js中的线程

在Node.js中,线程是指单个进程内的独立执行上下文,它是一个轻量级的处理单元,可以与同一进程中的其他线程并发操作。每个线程都有自己的执行指针和堆栈,并共享进程堆。


Node.js使用两种类型的线程:由事件循环管理的主线程和工作池中的多个辅助线程。(在本文中”辅助线程“和"线程"可互换使用来指代工作线程)

Node.js中的主线程是Node.js启动时的初始执行线程,它负责执行JavaScript代码并处理传入的请求,工作线程是与主线程并行运行的单独执行线程。


Node.js 以多线程还是单线程方式运行?
“单线程”是指只有一个执行线程的程序,允许它顺序执行任务,“多线程”意味着具有多个执行线程的程序可以同时执行任务。通常情况下,Node.js 被认为是单线程,因为它只有一个处理 JavaScript 操作和 I/O 的主事件循环。然而,Node.js单线程架构中的主要元素是事件循环,这使得 Node.js 尽管是单线程运行,却有着强大的性能。


事件循环

事件循环是一种注册将要执行的回调(函数)的机制,并与 JavaScript 代码在同一线程中运行。当 JavaScript 操作阻塞线程时,事件循环也会被阻塞。


工作池

工作池是一种执行模型,它生成并管理单独的线程,这些线程同步执行任务并将结果返回到事件循环。然后,事件循环使用结果执行提供的回调。工作池主要用于异步 I/O 操作,例如与系统磁盘和网络的交互,并在libuv中实现。尽管当 Node.js 需要在 JavaScript 和 C++ 之间进行内部通信时可能会出现轻微的延迟,但几乎不会被注意到。


使用事件循环和工作池实现异步操作
借助事件循环和工作池机制,能够在 Node.js 中编写有效处理异步操作的代码。
fs.readFile(path.join(__dirname, './package.json'), (err, content) => {
 if (err) {
   return null;
 }
 console.log(content.toString());
});


介绍worker_threads模块

worker_threads模块是一个包,它允许在单核上创建多个线程,下面是worker_threads的使用方法:
type WorkerCallback = (err: any, result?: any) => any;
export function runWorker(path: string, cb: WorkerCallback, workerData: object | null = null) {
const worker = new Worker(path, { workerData });
worker.on('message', cb.bind(null, null));
worker.on('error', cb);
worker.on('exit', (exitCode) => {
   if (exitCode === 0) {
     return null;
   }
   return cb(new Error(`Worker has stopped with code ${exitCode}`));
 });
 return worker;
}

先创建一个Worker类的实例,第一个参数包含worker代码的文件路径,第二个参数应该是一个包含名为workerData的属性的对象,并在开始执行时能够访问的数据。


需要注意的是,无论是使用 JavaScript 还是TypeScript,文件路径都应始终指向扩展名为 .js 或.mjs的文件。


下面是一些常见的事件:
JavaScript复制代码/*每当工作线程中发生未处理的异常时,会触发错误事件。随后,工作线程被终止,
并且可以将错误作为提供的回调函数中的第一个参数进行访问。这种设置可以实现及时捕获和处理异常情况。
*/
worker.on('error', (error) => {});

/*
当工作线程退出时,会发出exit事件。如果调用process.exit(),exitCode将提供给回调函数。
如果使用worker.terminate()终止worker ,退出代码将被设置为1:
*/
worker.on('exit', (exitCode) => {});

/*
当工作线程向父线程发送数据时,会发出消息事件。现在,来看看数据是如何在线程之间共享的。
*/
worker.on('online', () => {});


使用工作线程的两种方法:
工作程序

第一种方法是生成一个工作程序,执行其代码,并将结果发送回父级。然而,这种方法具有显着的开销成本,包括创建新的工作线程、管理每个线程的内存开销以及启动和管理线程所需的资源。虽然可以使用这种方法完成任务,但它可能效率不高,尤其是在大规模基于节点的系统中。为了解决与此方法相关的挑战,通常采用第二种更常用的行业实践。


工作线程池

第二种方法是实现工作线程池,它通过创建可重用于多个任务的工作线程池来减轻第一种方法的缺点。不是为每个任务创建一个新的工作线程,而是创建一个工作线程池,并将任务分配给它们。


用技术术语来说,工作池可以被视为管理工作线程池的抽象数据类型。池中的每个工作线程都被分配一个任务,并且该线程与其他线程并行执行该任务。

在工作池中分配任务的方式有多种,池充当管理器,将任务分配给工作线程,收集它们的结果,并促进池中线程之间的通信。


实现工作池可能涉及使用不同的数据结构和算法,例如任务队列和消息传递系统。具体数据结构的选择取决于多种因素,包括所需的工作线程数量、任务的性质以及线程之间所需的通信级别。


Node.js实现工作池
在 Node 中,可以使用内置功能或第三方工具来实现工作池。节点的内置工作线程模块提供对工作线程的支持,可用于创建工作池。此外,还有多个库可以通过为工作线程提供高级 API 以及对任务调度和线程管理的额外支持来补充工作池。这些库自动执行任务调度和线程管理过程,从而更容易实现工作池。
为了说明这一点,下面是一个利用Node内置工作线程功能的示例代码:
const { Worker, isMainThread, parentPort } = require('worker_threads');

if (isMainThread) {
  // 堆代码 duidaima.com
  // Main thread code
  // Create an array to store worker threads
  const workerThreads = [];
  // Create a number of worker threads and add them to the array
  for (let i = 0; i < 4; i++) {
    workerThreads.push(new Worker(__filename));
  }
  // Send a message to each worker thread with a task to perform
  workerThreads.forEach((worker, index) => {
    worker.postMessage({ task: index });
  });
} else {
  // Worker thread code
  // Listen for messages from the main thread
  parentPort.on('message', message => {
    console.log(`Worker ${process.pid}: Received task ${message.task}`);
    // Perform the task
    performTask(message.task);
  });
  function performTask(task) {
    // … operations to be performed to execute the task
  }
}

上面的代码由两部分组成:一部分用于主线程,另一部分用于工作线程。在主线程部分,从模块中导入必要的成员,如果当前执行上下文在主线程中,则创建一个数组来存储四个worker。随后,带有要执行的任务的新消息被发送到每个工作线程。


在工作线程部分,使用属性方法来监听来自主线程的消息parentPort。一旦收到消息,记录下进程ID和任务,并将任务传递给应用程序中适当的方法来执行。这样就能够更加智能地处理任务,并提供高效的函数执行。


使用线程的主要优点是什么?

线程是一个强大的工具,可以极大地影响程序的性能、响应能力和整体效率。在 Node.js 中,线程对于开发人员来说是一项很有价值的功能,因为它可以将进程拆分为多个独立的执行流。如果正确使用,线程可以提高程序的速度、效率和响应能力。


线程的优势:
1.提高性能:线程允许并发执行多个任务,与顺序运行任务相比,整体程序执行速度更快。
2.响应性:线程可以防止计算量大的任务阻塞或延迟其他操作的执行,确保程序保持对用户输入和其他任务的响应。
3.资源共享:Node.js 中的线程可以共享变量等资源,从而实现并发处理并加快程序执行速度。
4.易于编程:线程消除了 Node.js 中单线程架构的限制,使编程更加高效和可扩展。
提高可扩展性:线程可以轻松扩展,从而可以更轻松地构建高性能且可扩展的 Node.js 应用程序,这些应用程序可以轻松处理增加的负载。

结论
通过worker_threads模块,可以轻松地将多线程支持集成到应用程序中。将密集的CPU计算卸载到单独的线程中,可以大幅提高服务器的吞吐量。这种设计可以吸引更多来自人工智能、机器学习和大数据等领域的开发人员和工程师开始在他们的项目中使用Node.js。因此,使用worker_threads模块是一种高效、便捷的方式来实现多线程编程。

用户评论