چند روز پیش، نسخه 10.5.0 نود جیاس منتشر شد و یکی از امکانات اصلی که داشت، پشتیبانی اولیه (و آزمایشی) threadها بود.
این اتفاق جالبی است؛ مخصوصا با توجه به این که این زبان همیشه با توجه به async ورودی / خروجی خود، به عدم نیاز به threadها میبالید. پس چرا باید به threadها در Node نیاز داشته باشیم؟
پاسخ سریع و ساده آن این است: تا Node در تنها جایی که قبلا مشکل داشته است، بهتر شود؛ که آن مشکل محاسبههای متمرکز و سنگین CPU بود. این دلیل اصلی قوی نبودن Node.js در زمینههایی مثل هوش مصنوعی، یادگیری ماشین، علم دادهها و موارد مشابه است. تلاشهای زیادی برای حل این مشکل انجام میشوند، اما همچنان کارای مد نظر را در زمینههایی مثل استقرار میکروسرویسها نداریم.
چگونه از ماژول جدید threadها استفاده کنیم؟
برای شروع، به ماژولی به نام «worker-threads» نیاز خواهید داشت.
دقت کنید که این فقط زمانی کار میکند که از علامت --experimental-worker در هنگام اجرای اسکریپت استفاده کنید، در غیر این صورت ماژول مورد نظر پیدا نمیشود.
دقت کنید که علامت مورد نظر به threadها اشاره نمیکند، بلکه به workerها اشاره میکند. در طی این مقاله، به این صورت به آنها اشاره میکنیم: worker threadها یا فقط workerها.
اگر در گذشته از پردازشهای چندگانه استفاده کردهاید، شباهتهای زیادی در این روش خواهید دید. اما اگر تجربهای در آن زمینه ندارید، نگران نباشد، تا جایی که بتوانم توضیح خواهم داد.
چه کاری با استفاده از آنها میتوانید انجام دهید؟
همانطور که قبلا اشاره کردم، هدف از worker threadها، عملیاتهای متمرکز CPU است. استفاده از آنها برای ورودی / خروجی، منابع را هدر میدهد. به گفته سند اصلی Node، مکانیزم داخلی که توسط Node برای رسیدگی به ورودی / خروجی فراهم شده است، بسیار موثرتر از استفاده از یک worker thead برای آن است، پس خودتان را به زحمت نیدازید.
بیایید با مثال سادهای از ساخت یک worker و استفاده از آن شروع کنیم.
مثال ۱:
const { Worker, isMainThread, workerData } = require('worker_threads');
let currentVal = 0;
let intervals = [100,1000, 500]
function counter(id, i){
console.log("[", id, "]", i)
return i;
}
if(isMainThread) {
console.log("this is the main thread")
for(let i = 0; i < 2; i++) {
let w = new Worker(__filename, {workerData: i});
}
setInterval((a) => currentVal = counter(a,currentVal + 1), intervals[2], "MainThread");
} else {
console.log("this isn't")
setInterval((a) => currentVal = counter(a,currentVal + 1), intervals[workerData], workerData);
}
مثال بالا، مجموعهای از خطوط که شمارندههای افزایشی را نشان میدهند را خروجی میدهد، که مقدار آنها را در سرعتهای مختلف افزایش میدهد.
حال بیایید آن را جزئیتر درک کنیم:
- کد نوشته شده، در بیانیه IF، دو worker thread را ایجاد میکند. به واسطه پارامتر __filename، کد موجود در آنها از همان فایل گرفته شده است. Workerها به آدرس کامل فایل نیاز دارند، و این مقدار، به همین دلیل استفاده شده است.
- دو worker موجود، در قالب workerData که بخشی از آرگومان دوم است، به عنوان یک پارامتر global ارسال شده اند. این مقدار، بعدا از طریق یک constant با همان نام، در دسترس است. (به نحوه ساخت constant در خط اول و استفاده کردن از آن در خط آخر، دقت کنید.)
این مثال، یکی از اساسیترین کارها است که میتوانید با استفاده از این ماژول انجام دهید، اما آنچنان جالب نیست. بیایید نگاهی به یک مثال دیگر داشته باشیم.
مثال ۲: یک کار سنگین انجام دهید
حال بیایید سعی کنیم در حالیکه که در thread اصلی مقداری فعالیت async انجام میدهیم، مقداری محاسبه سنگین نیز انجام دهیم.
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');
const request = require("request");
if(isMainThread) {
console.log("This is the main thread")
let w = new Worker(__filename, {workerData: null});
w.on('message', (msg) => { //A message from the worker!
console.log("First value is: ", msg.val);
console.log("Took: ", (msg.timeDiff / 1000), " seconds");
})
w.on('error', console.error);
w.on('exit', (code) => {
if(code != 0)
console.error(new Error(`Worker stopped with exit code ${code}`))
});
request.get('http://www.google.com', (err, resp) => {
if(err) {
return console.error(err);
}
console.log("Total bytes received: ", resp.body.length);
})
} else { //the worker's code
function random(min, max) {
return Math.random() * (max - min) + min
}
const sorter = require("./test2-worker");
const start = Date.now()
let bigList = Array(1000000).fill().map( (_) => random(1,10000))
sorter.sort(bigList);
parentPort.postMessage({ val: sorter.firstValue, timeDiff: Date.now() - start});
}
این بار، ما از صفحه اصلی میخواهیم که Google.com را بیاورد، در حالیکه در همان زمان، آرایهای متشکل از ۱ میلیون عدد را مرتب میکنیم. این کار چند ثانیه زمان میبرد، و به همین دلیل برای نمایش نحوه رفتار این فعالیت بسیار مناسب است. همچنین میخواهیم مدت زمانی که worker threadها برای انجام این فعالیت نیاز دارند را اندازه بگیریم و مقدار به دست آمده را (به همراه مقدار مرتب شده) به thread اصلی بفرستیم، که در آنجا نیز نتایج را نمایش میدهیم.
چیزی که از این مثال به دست میآوریم، ارتباط میان threadها است.
Workerها میتوانند از طریق متد on پیامهایی را در thread اصلی دریافت کنند. رویدادهایی که میتوانیم استفاده کنیم، در کد نشان داده شدهاند. رویداد message هر زمانی که یک پیام را از thread اصلی با استفاده از متد parent.Port.postMessage میفرستیم، فعال میشود. همچنین میتوانید با استفاده از متد مشابه، بر روی workerهای خود یک پیام به کد thread بفرستید و با استفاده از آبجکت parentPort آن را بگیرید.
حال بیایید نگاهی به یک مثال بسیار مشابه، اما با کدی بسیار تمیزتر داشته باشیم و ببینید که چگونه میتوانید کد thread خود را ساختاربندی کنید.
مثال ۳: جمع آوری همه چیز در یک مکان
برای مثال آخر، بر روی همان عملکرد مشابه میمانیم، اما به شما نشان میدهیم که چگونه میتوانید آن را کمی تمیز کنید و نسخهای قابل نگهداریتر داشته باشید.
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');
const request = require("request");
function startWorker(path, cb) {
let w = new Worker(path, {workerData: null});
w.on('message', (msg) => {
cb(null, msg)
})
w.on('error', cb);
w.on('exit', (code) => {
if(code != 0)
console.error(new Error(`Worker stopped with exit code ${code}`))
});
return w;
}
console.log("this is the main thread")
let myWorker = startWorker(__dirname + '/workerCode.js', (err, result) => {
if(err) return console.error(err);
console.log("[[Heavy computation function finished]]")
console.log("First value is: ", result.val);
console.log("Took: ", (result.timeDiff / 1000), " seconds");
})
const start = Date.now();
request.get('http://www.google.com', (err, resp) => {
if(err) {
return console.error(err);
}
console.log("Total bytes received: ", resp.body.length);
//myWorker.postMessage({finished: true, timeDiff: Date.now() - start}) //you could send messages to your workers like this
})
و همچنین thread شما میتواند داخل یک فایل دیگر باشد. مثلا:
const { parentPort } = require('worker_threads');
function random(min, max) {
return Math.random() * (max - min) + min
}
const sorter = require("./test2-worker");
const start = Date.now()
let bigList = Array(1000000).fill().map( (_) => random(1,10000))
/**
//you can receive messages from the main thread this way:
parentPort.on('message', (msg) => {
console.log("Main thread finished on: ", (msg.timeDiff / 1000), " seconds...");
})
*/
sorter.sort(bigList);
parentPort.postMessage({ val: sorter.firstValue, timeDiff: Date.now() - start});
اگر بخواهیم آن را جزئیتر درک کنیم:
- حال thread اصلی و worker thread، کد مربوط به خود را در فایلهای جداگانه دارند. نگهداری و گسترش این مورد سادهتر است.
- تابع startWorker یک نمونه دیگر را بر میگرداند و شما را قادر میسازد تا اگر خواستید، بعدا پیامها را بفرستید.
- دیگر نیازی نیست که نگران باشید که کد thread اصلی شما در واقع همان thread اصلی است. (بیانیه IF اصلی را پاک کردیم)
- در کد worker میتوانید ببینید که چگونه میتوانید از thread اصلی یک پیام را دریافت کنید، و در نتیجه میتوانید یک ارتباط ناهمگام دو طرفه داشته باشید.
این مقاله در اینجا به پایان میرسد. امیدوارم درک کرده باشید که چگونه میتوانید از این ماژولها استفاده کنید. به یاد داشته باشید که این یک نسخه شدیدا آزمایشی است و مواردی که در این مقاله توضیح داده شدند، در آینده ممکن است تغییر کنند.
دیدگاه و پرسش
در حال دریافت نظرات از سرور، لطفا منتظر بمانید
در حال دریافت نظرات از سرور، لطفا منتظر بمانید