Node.js با وجود Single thread بودنش، چطور تسکهای سنگین رو انجام میده؟🤔
(مدیونید اگه احساس کنید پست طولانیه و نخونید. خدا شاهده از سر و تهش کلی زدم طولانی نیست بخون به درد میخوره 😂❤️)
این بزرگوار (Node.js) با وجود اینکه تکنخ (Single Thread) هست، پشت صحنه یه Thread Pool داره که عصای دستشه. یعنی چی حالا؟
ما میدونیم که Node.js یک رانتایم غیرهمزمان (asynchronous) هست. یعنی وقتی داره خط به خط کدها رو اجرا میکنه، اگه به یه عملیات سنگین یا زمان بر برسه، کل نخ اصلی رو نگه نمیداره تا اون عملیات تموم بشه. پس چی کار میکنه؟
(پیام بازرگانی :
ببینید تسکهای زمان بر کلاً دو دسته اند. یه سریشون که سنگینن و نیاز به پردازش CPU دارن (مثل خوندن فایل یا رمزنگاری)، میرن به Thread Pool. یه سری دیگه که بیشتر منتظر I/O هستن (مثل درخواست HTTP یا اتصال به دیتابیس)، به سیستمعامل سپرده میشن و اصلاً به Thread Pool نیازی ندارن. مثلاً Promise هاتوی خود نخ اصلی توسط Event Loop مدیریت میشن و به Thread Pool نمیرن، مگه اینکه داخلشون یه عملیات سنگین (مثل رمزنگاری) باشه. خلاصه که Node.js هر تسک رو میفرسته جای درستش. خودش هم میره سراغ اجرای خط بعدی کد. )
Event Loop چیه؟
Event Loop قلب تپندهی Node.js هست. یه حلقهی بی انتهاست که مدام چک میکنه آیا تسک جدیدی توی Event Queue (صف رویدادها) هست یا نه. اگه باشه، اون تسک رو بر میداره و اجرا میکنه. اگه نباشه، منتظر میمونه تا یه تسک جدید بیاد. یعنی بیچاره نخ اصلی هیچوقت بیکار نمیمونه.
Event Queue چیه؟
اینطوری در نظر بگیریم که یه صف هست که تمام کارهایی که منتظر اجرا هستن توش وایستادن. هر وقت یه عملیات غیرهمزمان (مثل پاسخ یه درخواست HTTP یا خوندن یه فایل) تموم میشه، نتیجهش میاد تو این صف و Event Loop اونو بر میداره و اجرا میکنه.
Thread Pool چطور کار میکنه؟اینهمه پیش زمینه گفتیم که برسیم به اصل موضوع این پست.
برای عملیات سنگین (مثل دسترسی به فایلسیستم، فشردهسازی دادهها یا رمزنگاری)، Node.js از یه Thread Pool که معمولاً ۴ تا نخ داره استفاده میکنه. این نخها جدا از نخ اصلی هستن و توسط libuv (یک لایبرری نوشته شده با C) مدیریت میشن. وقتی یه تسک سنگین به Node.js میدیم ، اونو میفرسته به Thread Pool، و وقتی کار تموم شد، نتیجه رو برمیگردونه به Event Queue تا نخ اصلی بتونه پردازشش کنه.
این معماری باعث میشه Node.js برای برنامههایی که ورودی/خروجی (I/O) زیادی دارن (مثل وبسرورها) خیلی سریع و بهینه باشه. اما اگه تسکها خیلی سنگین باشن (مثل محاسبات پیچیده)، ممکنه Thread Pool پر بشه و عملکرد افت کنه. برای اینجور کارا معمولاً از ابزارهای دیگه یا Worker Threads استفاده میشه.
#nodejs
(مدیونید اگه احساس کنید پست طولانیه و نخونید. خدا شاهده از سر و تهش کلی زدم طولانی نیست بخون به درد میخوره 😂❤️)
این بزرگوار (Node.js) با وجود اینکه تکنخ (Single Thread) هست، پشت صحنه یه Thread Pool داره که عصای دستشه. یعنی چی حالا؟
ما میدونیم که Node.js یک رانتایم غیرهمزمان (asynchronous) هست. یعنی وقتی داره خط به خط کدها رو اجرا میکنه، اگه به یه عملیات سنگین یا زمان بر برسه، کل نخ اصلی رو نگه نمیداره تا اون عملیات تموم بشه. پس چی کار میکنه؟
(پیام بازرگانی :
ببینید تسکهای زمان بر کلاً دو دسته اند. یه سریشون که سنگینن و نیاز به پردازش CPU دارن (مثل خوندن فایل یا رمزنگاری)، میرن به Thread Pool. یه سری دیگه که بیشتر منتظر I/O هستن (مثل درخواست HTTP یا اتصال به دیتابیس)، به سیستمعامل سپرده میشن و اصلاً به Thread Pool نیازی ندارن. مثلاً Promise هاتوی خود نخ اصلی توسط Event Loop مدیریت میشن و به Thread Pool نمیرن، مگه اینکه داخلشون یه عملیات سنگین (مثل رمزنگاری) باشه. خلاصه که Node.js هر تسک رو میفرسته جای درستش. خودش هم میره سراغ اجرای خط بعدی کد. )
Event Loop چیه؟
Event Loop قلب تپندهی Node.js هست. یه حلقهی بی انتهاست که مدام چک میکنه آیا تسک جدیدی توی Event Queue (صف رویدادها) هست یا نه. اگه باشه، اون تسک رو بر میداره و اجرا میکنه. اگه نباشه، منتظر میمونه تا یه تسک جدید بیاد. یعنی بیچاره نخ اصلی هیچوقت بیکار نمیمونه.
Event Queue چیه؟
اینطوری در نظر بگیریم که یه صف هست که تمام کارهایی که منتظر اجرا هستن توش وایستادن. هر وقت یه عملیات غیرهمزمان (مثل پاسخ یه درخواست HTTP یا خوندن یه فایل) تموم میشه، نتیجهش میاد تو این صف و Event Loop اونو بر میداره و اجرا میکنه.
Thread Pool چطور کار میکنه؟اینهمه پیش زمینه گفتیم که برسیم به اصل موضوع این پست.
برای عملیات سنگین (مثل دسترسی به فایلسیستم، فشردهسازی دادهها یا رمزنگاری)، Node.js از یه Thread Pool که معمولاً ۴ تا نخ داره استفاده میکنه. این نخها جدا از نخ اصلی هستن و توسط libuv (یک لایبرری نوشته شده با C) مدیریت میشن. وقتی یه تسک سنگین به Node.js میدیم ، اونو میفرسته به Thread Pool، و وقتی کار تموم شد، نتیجه رو برمیگردونه به Event Queue تا نخ اصلی بتونه پردازشش کنه.
این معماری باعث میشه Node.js برای برنامههایی که ورودی/خروجی (I/O) زیادی دارن (مثل وبسرورها) خیلی سریع و بهینه باشه. اما اگه تسکها خیلی سنگین باشن (مثل محاسبات پیچیده)، ممکنه Thread Pool پر بشه و عملکرد افت کنه. برای اینجور کارا معمولاً از ابزارهای دیگه یا Worker Threads استفاده میشه.
#nodejs
❤1
Forwarded from Node Master (Iman Hosseini Pour)
همیشه ما همه تلاش داریم کد با Performance خوب توسعه بدیم بدون این که این موضوع رو تصور کنیم که داخل کدبیس های #JavaScript معمولا Performance شوخیه.
ولی امروز قراره درمورد یک ویژگی جدید که در آپدیت ES2025 به #JavaScript اضافه شده صحبت کنیم که بهمون کمک میکنه که Performance بهتری داشته باشیم. سمت #NodeJS در بیزینس لاجیک های پیچیده میتونه معجزه کنه. برای #FrontEnd هم کاربردی هست ولی باتوجه به این که مرورگر های قدیمی ساپورت نمیکنن خب قطعا به این زودی استفاده ازش رو نمیبینیم.
ویژگی جدید ما اضاف شدن یک static method جدید به Iterator هست.
حالا سوال پیش میاد که چطور این به ما کمک میکنه. فرض کنید یک array بزرگ دارید و میخواید data رو map کنید به یک شکل دیگه و برای این کار یک pipeline از map ها رو ایجاد کردید:
در نگاه اول مشکلی نداره ولی اگر با عینک Performance ببینیم دوتا مشکل میبینیم.
1. برای هر map مجبوریم یکبار کامل loop بزنیم خب 3 بار loop میزنیم پس داریم O(3n)
2. هربار که یک loop کامل میزنیم هربار داریم یک Array جدید بعد از map ایجاد میکنیم. به صورت خلاصه هر .map برابر هست با یک Array allocation جدید. خب اینجا یک array اورجینال داریم و 3 تا map پس 4 تا array allocation داریم.
ممکنه برای تازه کارترها سوال های زیر پیش بیاد:
1. خب چرا اصلا این استایل کد میزنیم؟
2. چرا همه رو داخل یک map انجام نمیدیم؟
پاسخ سوال اول:
- میتونیم با for ... of کار رو بهتر با یک loop در بیاریم ولی مسئله این هست که معمولا برنامه نویس های #JavaScript در اینجور مواقع حتی بدون این که خودشون بدونن دیدگاه Functional Programming دارن و خب از اونجایی که به صورت فلسفی FP ذات Declarative داره و به صورت فلسفی کار کردن با API ها Declarative خیلی راحت تر و لذت بخش تر از Imperative هست همچین چیزی رو میبینیم.
- اگر هم خیلی کنجکاوید بیشتر بدونید وقتش هست نگاهی به #Elixir #Scala یا حتی Lambda ها در #Java اونجا قشنگ متوجه میشید. یا اصلا مسیر رو برعکس برید و نگاهی به رویکرد #Golang کنید و فرق زمین تا آسمونی رو ببینید.
پاسخ سوال دوم:
- در بزینس لاجیک های پیچیده برای خوانایی کد داشتن map های بیشتر خیلی بهتر از این هست که یک map بزرگ داشته باشیم. منطقی هم هست چون خیلی بهمون God Object ها رو یادآوری میکنه.
خب حالا سوال پیش میاد چیکار کنیم؟ خیلی ساده هست کافیه فقط خط اول رو به این شکل عوض کنیم و array رو تبدیل کنیم به Iterator.
خب دوباره الان سوال پیش میاد که WTF الان چی شد؟ سادس.
ما دیتا رو تبدیل کردیم به یک Iterator که ذات Iterator ها به صورت Lazy هست یعنی تا وقتی که نیاز به consume شدن data نباشه هیچ پردازشی انجام نمیشه و اگر هم نیاز به map کردن باشه دقیقا در runtime به صورت on-demand برای هر index تبدیل انجام میشه و ما نیازی به alloc کردن حافظه اضافه برای Array نداریم و هیچ loop اضافه ای هم درکار نیست.
حالا به این نکات توجه کنید:
- هر iterator رو فقط یکبار میشه consume کرد و اگر نیاز باشه باید دوباره ازش بسازی. در حقیقت با .toArray داریم consume کردن رو شبیه سازی میکنیم و دومی مقدار خالی به ما میده به خاطر iterator بودن.
- در این قسمت به map ها باید توجه کرد که با هر بار call شدن یک Array جدید نمیسازن بلکه یک Iterator جدید که روی Iterator قبلی سوار هست رو به ما میده! پس در نتیجه با توجه به تعریف Iterator که بالاتر گفتم نه loop اضافه ای داریم و نه alloc اضافه.
حالا اگر یکم بیشتر دقت کنی میبینیم خیلی شبیه به stream ها هست. اصلا این دوتا api به شدت باهم سازگار هستن. در این حد که استریم ها رو میشه تبدیل کرد به iterator و برعکس. بقیه کد هم دقیقا به صورت مشابهه کار میکنه.
دل نوشته:
حقیقتا دیگ نمیشه تفاوت بین stream, iterator, generator, rxjs, web stream, رو تشخیص داد😂. همشون رو میتونی جایگزین هم استفاده کنی. ( دلایل مختلفی برای وجود این همه api برای یک کار هست )
ولی امروز قراره درمورد یک ویژگی جدید که در آپدیت ES2025 به #JavaScript اضافه شده صحبت کنیم که بهمون کمک میکنه که Performance بهتری داشته باشیم. سمت #NodeJS در بیزینس لاجیک های پیچیده میتونه معجزه کنه. برای #FrontEnd هم کاربردی هست ولی باتوجه به این که مرورگر های قدیمی ساپورت نمیکنن خب قطعا به این زودی استفاده ازش رو نمیبینیم.
ویژگی جدید ما اضاف شدن یک static method جدید به Iterator هست.
Iterator.from()
حالا سوال پیش میاد که چطور این به ما کمک میکنه. فرض کنید یک array بزرگ دارید و میخواید data رو map کنید به یک شکل دیگه و برای این کار یک pipeline از map ها رو ایجاد کردید:
const data = [1, 2, 3, 4, 5];
const final = data
.map((item) => item.toString())
.map((item) => `- ${item} -`)
.map((item) => `${item} ${new Date()}`);
در نگاه اول مشکلی نداره ولی اگر با عینک Performance ببینیم دوتا مشکل میبینیم.
1. برای هر map مجبوریم یکبار کامل loop بزنیم خب 3 بار loop میزنیم پس داریم O(3n)
2. هربار که یک loop کامل میزنیم هربار داریم یک Array جدید بعد از map ایجاد میکنیم. به صورت خلاصه هر .map برابر هست با یک Array allocation جدید. خب اینجا یک array اورجینال داریم و 3 تا map پس 4 تا array allocation داریم.
ممکنه برای تازه کارترها سوال های زیر پیش بیاد:
1. خب چرا اصلا این استایل کد میزنیم؟
2. چرا همه رو داخل یک map انجام نمیدیم؟
پاسخ سوال اول:
- میتونیم با for ... of کار رو بهتر با یک loop در بیاریم ولی مسئله این هست که معمولا برنامه نویس های #JavaScript در اینجور مواقع حتی بدون این که خودشون بدونن دیدگاه Functional Programming دارن و خب از اونجایی که به صورت فلسفی FP ذات Declarative داره و به صورت فلسفی کار کردن با API ها Declarative خیلی راحت تر و لذت بخش تر از Imperative هست همچین چیزی رو میبینیم.
- اگر هم خیلی کنجکاوید بیشتر بدونید وقتش هست نگاهی به #Elixir #Scala یا حتی Lambda ها در #Java اونجا قشنگ متوجه میشید. یا اصلا مسیر رو برعکس برید و نگاهی به رویکرد #Golang کنید و فرق زمین تا آسمونی رو ببینید.
پاسخ سوال دوم:
- در بزینس لاجیک های پیچیده برای خوانایی کد داشتن map های بیشتر خیلی بهتر از این هست که یک map بزرگ داشته باشیم. منطقی هم هست چون خیلی بهمون God Object ها رو یادآوری میکنه.
خب حالا سوال پیش میاد چیکار کنیم؟ خیلی ساده هست کافیه فقط خط اول رو به این شکل عوض کنیم و array رو تبدیل کنیم به Iterator.
const data = [1, 2, 3, 4, 5, 6]; ❌
const data = Iterator.from([1, 2, 3, 4, 5, 6]); ✅
خب دوباره الان سوال پیش میاد که WTF الان چی شد؟ سادس.
ما دیتا رو تبدیل کردیم به یک Iterator که ذات Iterator ها به صورت Lazy هست یعنی تا وقتی که نیاز به consume شدن data نباشه هیچ پردازشی انجام نمیشه و اگر هم نیاز به map کردن باشه دقیقا در runtime به صورت on-demand برای هر index تبدیل انجام میشه و ما نیازی به alloc کردن حافظه اضافه برای Array نداریم و هیچ loop اضافه ای هم درکار نیست.
حالا به این نکات توجه کنید:
- هر iterator رو فقط یکبار میشه consume کرد و اگر نیاز باشه باید دوباره ازش بسازی. در حقیقت با .toArray داریم consume کردن رو شبیه سازی میکنیم و دومی مقدار خالی به ما میده به خاطر iterator بودن.
const data = Iterator.from([1, 2, 3, 4, 5, 6]);
data.toArray()
data.toArray()
- در این قسمت به map ها باید توجه کرد که با هر بار call شدن یک Array جدید نمیسازن بلکه یک Iterator جدید که روی Iterator قبلی سوار هست رو به ما میده! پس در نتیجه با توجه به تعریف Iterator که بالاتر گفتم نه loop اضافه ای داریم و نه alloc اضافه.
const data = Iterator.from([1, 2, 3, 4, 5, 6]);
const final = data
.map((item) => item.toString())
.map((item) => `- ${item} -`)
.map((item) => `${item} ${new Date()}`);
حالا اگر یکم بیشتر دقت کنی میبینیم خیلی شبیه به stream ها هست. اصلا این دوتا api به شدت باهم سازگار هستن. در این حد که استریم ها رو میشه تبدیل کرد به iterator و برعکس. بقیه کد هم دقیقا به صورت مشابهه کار میکنه.
import { Readable } from "node:stream";
const data = Iterator.from([1, 2, 3, 4, 5, 6]);
const streamData = Readable.from(data);دل نوشته:
حقیقتا دیگ نمیشه تفاوت بین stream, iterator, generator, rxjs, web stream, رو تشخیص داد😂. همشون رو میتونی جایگزین هم استفاده کنی. ( دلایل مختلفی برای وجود این همه api برای یک کار هست )
👍3
🔥 اجرای دستورات سرور از دل Node.js
زمانی که توی پروژههای Node.js کار میکنید، حتماً(حالا نه حتماً ولی شاید) پیش اومده(یا شاید پیش بیاد) که بخواید یه دستور ترمینال (Shell) رو از داخل کدتون اجرا کنید. مثلاً یه اسکریپت پایتون رو ران کنید، با
درسته که با
اینجاست که میتونیم از یک ابزار قدرتمندتر به نام
فرض کنید میخوایم دستور ping google.com رو اجرا کنیم و خروجی رو همون لحظه ببینیم.
#server #nodejs
زمانی که توی پروژههای Node.js کار میکنید، حتماً(حالا نه حتماً ولی شاید) پیش اومده(یا شاید پیش بیاد) که بخواید یه دستور ترمینال (Shell) رو از داخل کدتون اجرا کنید. مثلاً یه اسکریپت پایتون رو ران کنید، با
ffmpeg یه ویدیو رو پردازش کنید یا حتی وضعیت یه سرویس رو چک کنید.درسته که با
exec میتونیم اینکار رو کنیم ،اما exec یه مشکلی که داره باید صبر کنیم تا دستور کامل تموم بشه.که ما اعصابش رو نداریم.اینجاست که میتونیم از یک ابزار قدرتمندتر به نام
spawn از ماژول داخلی child_process بهره مند بشیم!
spawn به جای منتظر موندن، یه فرآیند جدید ایجاد میکنه و به شما اجازه میده خروجی (و حتی خطاها) رو به صورت زنده و لحظه ای دریافت کنید.فرض کنید میخوایم دستور ping google.com رو اجرا کنیم و خروجی رو همون لحظه ببینیم.
import { spawn } from 'child_process';
// دستور اصلی 'ping' و آرگومانش 'google.com'
const process = spawn('ping', ['google.com']);
// 1. گوش دادن به خروجی موفقیتآمیز (stdout)
// این قسمت با هر خط جدیدی که در خروجی چاپ بشه، اجرا میشه
process.stdout.on('data', (data) => {
console.log(`[LOG]: ${data}`);
});
// 2. گوش دادن به خروجی خطا (stderr)
process.stderr.on('data', (data) => {
console.error(`[ERROR]: ${data}`);
});
// 3. وقتی فرآیند کاملاً تموم شد
process.on('close', (code) => {
console.log(`✅ فرآیند با کد خروجی ${code} بسته شد.`);
});#server #nodejs
❤2👍1