Thread ها در Node 10.5.0: یک شروع عملی

چند روز پیش ورژن ۱۰.۵.۰ Node.js منتشر شد و یکی از قابلیت های اصلی که در اون اضافه شده بود پشتیبانی آزمایشی از Thread ها بود.

این موضوع زمانی جذاب میشه که در مورد زبانی صحبت می کنیم که همیشه به خاطر عدم نیازش به Thread ها و Async I/O محبوب و خارق العاده ش به خودش غرّه بوده.

ولی چرا اصلا Node باید به Thread ها نیاز داشته باشه؟

جواب ساده و کوتاه اینه که: برای بهتر شدن تو زمینه هایی که Node قبلا تو گذشته در اونها رنج کشیده یا پردازش هایی که نیاز به استفاده های سنگین از CPU دارن. به خاطر همین موضوع هست که Node.js توی زمینه هایی مثل AI، یادگیری ماشین یا داده پردازی و زمینه های اینچنینی خیلی قوی نیست. تلاش های زیادی برای بهبود این موضوع صورت گرفته ولی هنوز نمی تونیم به خوبی زمانی از Node استفاده کنیم که میکروسرویس ها رو deploy می کنیم.


پس در این مقاله تلاش می کنیم که مستندات فنی که توسط PR اولیه و مستندات رسمی توضیح داده شده اند رو به صورت عملی تر و به کمک یک سری مثال های ساده توضیح بدیم. این احتمالا به شما کمک می کنه که از این قابلیت جدید استفاده کنید.


اما چجوری از ماژول جدید Thread ها استفاده کنیم؟

برای شروع با require کردن ماژولی به اسم "worker_threads" شروع می کنیم.

این موضوع رو به خاطر داشته باشین که تمام کد ها تنها زمانی کار می کنن که از flag مخصوص --experimental-worker موقع اجرای اسکریپت ها استفاده کنید وگرنه ماژول نامبرده پیدا نمیشه.

توجه کنین که flag به worker اشاره می کنه و نه thread ها، که دقیقا به همین شکل هم در سرتاسر مستندات بهش اشاره میشه: worker threads یا به صورت خلاصه تر workers.

اگر قبلا از multi-processing استفاده کردین شباهت های زیادی باهاش پیدا می کنین وگرنه نگران نباشید و تا هر مقدار که بشه توضیح میدم.

خوب با Thread اصن چیکار میشه کرد؟

Worker Thread ها همونطور که قبلا گفتم برای کارهایی که CPU زیادی مصرف می کنن ساخته شدن و استفاده ازشون برای I/O فقط هدررفت منابع به حساب میاد چون که بر اساس مستندات رسمی، مکانیزم داخلی Node برای async I/O خیلی بیشتر از worker thread ها بهینه است، پس خودتون رو اذیت نکنین.

بزارین با یه مثال ساده شروع کنیم که توش Worker میسازیم و ازش استفاده می کنیم.

مثال اول:

https://gist.github.com/deleteman/aa19d01fee124239ebf8fd2f5f6134b6

مثال بالا خروج ساده ای داره که در اون خط هایی شمارنه ها یا افزایشی رو نشون میدن که مقادیرشون با سرعت های متفاوت افزایش پیدا می کنه

خروجی کد بالا
خروجی کد بالا

بزارین جزییات بیشتری بگیم:

کد داخل IF دو worker thread میسازه و کدی که برای اون ها استفاده شده از یک فایل گرفته میشه چون پارامتر __filename رو بهش تحویل دادیم. Worker ها فعلا نیاز به مسیر های کامل به فایل ها دارن و با مسیر های وابسته (Relative Paths) سازگاری ندارن برای همین این پارامتر بهش داده شده.

هر دو worker به عنوان مقادیری global در آرگومان دوم به عنوان workerData پاس داده شدن. دسترسی به این مقدار می تونه به عنوان یک ثابت با اسمی یکسان وجود داشته باشه (ببینید که چطوری ثابت، توی خط اول فایل ساخته شده و بعدا در خط آخر از اون استفاده میشه.)

این مثال یکی از ساده ترین هایی که می تونید با استفاده از این ماژول ازش بهره بگیرید،‌ ولی به نظر جالب میاد نه؟ بزارین یه نگاهی به مثال دیگه بندازیم.
مثال دوم، واقعا یه کاری بکنیم.

بیاین تلاش کنیم واقعا وقتی داریم یه سری کارای async توی thread اصلی می کنیم یک سری محاسبات «سنگین» هم انجام بدیم.

https://gist.github.com/deleteman/c31a9704f9cbe3b15c45bb439a4c0da2

این بار ولی، داریم به صفحه اصلی گوگل درخواست می زنیم و همزمان یک آرایه شامل ۱ میلیون عدد که اتفاقی ساخته شده اند رو مرتب می کنیم. این کار چند ثانیه طول می کشه، پس مهمه که نشون بدیم این ماژول چقدر خوب کار می کنه. همچنین می خوایم زمانی که worker thread برای اجرای مرتب سازی میگیره رو اندازه گیری کنیم و در کنارم مقادیر مرتب شده به thread اصلی بفرستیم جایی که نتیجه رو نشون میدیم.

نتیجه مثال دوم
نتیجه مثال دوم


اصلی ترین خروجی این مثال ارتباط بین thread هاست. Worker ها می تونن پیام هایی رو از طریق متد on به thread اصلی بفرستن. Event هایی که می تونیم بهشون listen کنیم رو میشه توی کد دید. event به اسم message زمانی رخ میده که پیامی از یک thread با استفاده از متد parentPort.postMessage بفرستیم. همچنین میشه پیامی رو به کد thread با استفاده از همین متد،‌ روی worker فرستاد و با استفاده از parentPort دریافتش کرد.

اگه کنجکاو شدین، کدی ماژول helper ـی که من استفاده می کنم اینجاست، اگرچه چیز خاصی در موردش وجود نداره.

بیاین نگاهی بندازیم به یه مثال مشابه ولی با کد تمیز تر که یک ایده نهایی از اینکه چجوری باید به کد های worker thread ها ساختار داد میده.

مثال ۳: همه چی کنار هم

به عنوان یک مثال نهایی می خوام به همون عملکرد بپردازیم ولی نشون بدیم چطوری می تونیم یک نسخه که قابل نگهداریه با تمیز کردنش ازش بسازیم.

https://gist.github.com/deleteman/8e0c6d51c6875e1fc0e147c31e1d6897


وکدمربوطبهthreadهامیتونهتوییهفایلدیگهباشه،مثل:


https://gist.github.com/deleteman/df2119b1d386d3de3f2d86bad12ff32e


با نگاه به جزییات می فهمیم که:

  • Thread اصلی و worker thread ها حالا کد خودشون رو اینبار در فایل های متفاوت دارن که بهتر قابلیت نگه داری و توسعه داره.
  • تابع startWorker حالا یک شاخه جدید از worker بر می گردونه که اجازه میده بعدا اگر نیاز بود بهش پیام بفرستید.
  • اگر کد های thread اصلی واقعا همون thread اصلی باشن دیگه نیازی به نگرانی نیست (ما شرط IF رو حذف کردیم.)
  • میشه تو کد های worker دید که چه طوری میشه پیام ها رو از thread اصلی گرفت که اجازه میده ارتباط دو طرفه async بوجود بیاریم.

خوب واسه این مقاله دیگه کافیه، امیدوارم به اندازه کافی مقاله رو متوجه شده باشین که بهتون کمک کنه با این ماژول جدید شروع به کار کنید. ولی یادتون باشه که:

  • این ماژول هنوز شدیدا تو حالت آزمایش قرار داره و چیزایی که اینجا توضیح داده شده ممکنه توی انتشار های آینده تغییر کنه.
  • نظرات PR و مستندات رو بخونید،‌چرا که اطلاعات بیشتری در این باره اونجا موجوده و من فقط روی قدم های پایه ای برای شروع به کار تمرکز کردم.
  • ازش لذت ببرین! یه خورده باش ور برین، باگ هاش رو گزارش بدین و واسه بهتر شدنش پیشنهاد بدین چرا که این تازه شروعشه!


تا مقاله بعدی!