最初,JavaScript是用于设计执行简单的web任务的,比如表单验证。直到2009年,Node.js的创建者Ryan Dahl让开发人员认识到了通过JavaScript 进行后端开发已成为可能,在后端开发中,用到最多的就是多线程以及线程之间的同步功能,今天小编就为大家介绍一下如何使用Node.js实现多线程的应用。
在介绍之前,先给大家介绍一下Node.js的工作原理,Node.js基于单线程事件循环的范例进行操作。为了充分掌握Node.js的功能,理解Node中线程(构成Node.js核心的事件循环)的概念至关重要。
在Node.js中,线程是指单个进程内的独立执行上下文,它是一个轻量级的处理单元,可以与同一进程中的其他线程并发操作。每个线程都有自己的执行指针和堆栈,并共享进程堆。
Node.js中的主线程是Node.js启动时的初始执行线程,它负责执行JavaScript代码并处理传入的请求,工作线程是与主线程并行运行的单独执行线程。
事件循环是一种注册将要执行的回调(函数)的机制,并与 JavaScript 代码在同一线程中运行。当 JavaScript 操作阻塞线程时,事件循环也会被阻塞。
工作池是一种执行模型,它生成并管理单独的线程,这些线程同步执行任务并将结果返回到事件循环。然后,事件循环使用结果执行提供的回调。工作池主要用于异步 I/O 操作,例如与系统磁盘和网络的交互,并在libuv中实现。尽管当 Node.js 需要在 JavaScript 和 C++ 之间进行内部通信时可能会出现轻微的延迟,但几乎不会被注意到。
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的文件。
*/ worker.on('error', (error) => {}); /*当工作线程退出时,会发出exit事件。如果调用process.exit(),exitCode将提供给回调函数。
*/ worker.on('exit', (exitCode) => {}); /*当工作线程向父线程发送数据时,会发出消息事件。现在,来看看数据是如何在线程之间共享的。
*/ worker.on('online', () => {});
第一种方法是生成一个工作程序,执行其代码,并将结果发送回父级。然而,这种方法具有显着的开销成本,包括创建新的工作线程、管理每个线程的内存开销以及启动和管理线程所需的资源。虽然可以使用这种方法完成任务,但它可能效率不高,尤其是在大规模基于节点的系统中。为了解决与此方法相关的挑战,通常采用第二种更常用的行业实践。
第二种方法是实现工作线程池,它通过创建可重用于多个任务的工作线程池来减轻第一种方法的缺点。不是为每个任务创建一个新的工作线程,而是创建一个工作线程池,并将任务分配给它们。
在工作池中分配任务的方式有多种,池充当管理器,将任务分配给工作线程,收集它们的结果,并促进池中线程之间的通信。
实现工作池可能涉及使用不同的数据结构和算法,例如任务队列和消息传递系统。具体数据结构的选择取决于多种因素,包括所需的工作线程数量、任务的性质以及线程之间所需的通信级别。
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 中,线程对于开发人员来说是一项很有价值的功能,因为它可以将进程拆分为多个独立的执行流。如果正确使用,线程可以提高程序的速度、效率和响应能力。