در Node.js یک محیط اجرا single-thread برای اجرای برنامه ها داریم که سبب می شود برنامه هایی که از CPU استفاده زیادی دارند thread اصلی را بلاک کرده و در اجرای برنامه وقفه زمانی ایجاد کنند. کتابخانه worker_threads این مشکل را با فراهم کردن مکانیسمی برای اجرای کد به صورت موازی با thread اصلی مرتفع کرده است.
در پست کار با Worker Thread در Node.js با worker-thread ها و همچنین با نحوه استفاده از آنها در پروژه ها آشنا شدید. در این پست به برخی مزایا و معایب worker-thread ها اشاره می کنیم. همچنین برخی از کتابخانه ها را معرفی می کنیم که کار با worker-thread ها را برای ما راحت تر می کند.
مزایای worker-thread را می توان به این صورت خلاصه کرد:
تنها راه حل برای پیاده سازی multithreading در Node.js استفاده از Worker-thread ها است. عملیات های درگیر با CPU، پردازش های background و هر گونه اجرای کد به صورت همزمان (به غیر از اعمال I/O ) نیاز به استفاده از worker-thread ها دارند.
استفاده از worker-thread با چندین چالش همراه است که باید قبل از استفاده از worker ها از این موارد آگاه باشید، زیرا در برخی از موقعیت ها نباید از آن استفاده کرد.
اولین و برجسته ترین محدودیت worker thread ها این است که آنها thread های واقعی نیستند. برنامه های multithreading واقعی امکان اجرای همزمان چندین thread را که داده های یکسانی دارند را فراهم می کنند. در این سناریو، حافظه به روز شده در یک thread برای بقیه thread ها نیز قابل مشاهده خواهد بود و پیاده سازی یک برنامه multithreading نیاز به مدیریت دقیق حافظه برای جلوگیری از ایجاد race condition دارد.
در Node.js هنگام کار با worker thread ها فرآیندهای فرزندی ایجاد می شود که مستقل از کد جاوا اسکریپت در فرآیند اصلی عمل می کنند. در واقع با ایجاد یک محیط ایزوله که بر اساس مفسر V8 جاوا اسکریپت کار می کند. به همین دلیل محیط جدید می تواند برای اجرای یک فایل جاوا اسکریپت خارج از event loop اصلی استفاده شود.
از آنجایی که فایل به عنوان یک فرآیند فرزند اجرا می شود، هیچ اشتراک حافظه ای بین برنامه اصلی و worker-thread وجود ندارد. برای رفع این مشکل یک سیستم تبادل داده مبتنی بر رویداد(event) ارائه شده است تا بتوان مقادیر را بین فرآیندها رد و بدل کرد.
در نمونه کد زیر می توانید نحوه تبادل داده با استفاده از event را مشاهده کنید:
const { Worker, isMainThread, parentPort, workerData } = require("worker_threads"); if (isMainThread) { const worker = new Worker(__filename, {workerData: "hello world!"}); worker.on("message", msg => console.log(`Message received: ${msg}`)); worker.on("error", err => console.error(error)); worker.on("exit", code => console.log(`Worker exited with code ${code}.`)); } else { const data = workerData; parentPort.postMessage(`New data is \"${data}\".`); }
ایجاد یک worker-thread مانند ایجاد یک thread جدید در یک زبان multithreading نیست. هر worker-thread یک کپی اختصاصی از مفسر V8 جاوا اسکریپت را اجرا می کند، بنابراین استفاده از تعداد زیاد worker-thread ها منابع قابل توجهی را در هاست شما مصرف می کند.
اگرچه worker-thread ها به سرعت شروع به کار می کنند، اما همیشه یک سربار مرتبط وجود دارد. این یک عملیات نسبتاً گران است که worker-thread ها را برای کارهای سبک نامناسب می کند. در واقع بهترین کاندید برای فعالیت های پردازش موازی محدود به CPU می باشند که سبب صرفه جویی بسیار در هزینه ها و سرعت اجرا برنامه می شود.
میتوانید ناکارآمدیها را با استفاده مجدد از مجموعهای از wokret-thread ها کاهش دهید، که به شما امکان میدهد از متحمل شدن مداوم هزینههای ایجاد worker-thread های جدید جلوگیری کنید. برخی کتابخانهها پیچیدگی مدیریت یک worker-thread آسان تر میکنند.
ماهیت worker-thread ها به گونه ای است که برای وظایف I/O مناسب نیستند. برای خواندن یک فایل یا واکشی دادهها از طریق شبکه، به worker-thread نیازی ندارید – استفاده از توابع داخلی در Node.js برای این منظور بهتر است.
مستندات worker_threads به طور خاص توصیه می کند که از ماژول worker_threads برای این کارها استفاده نکنید. هزینه ایجاد و نگهداری فرآیند worket-thread با مفسر V8 خود بسیار بیشتر از پیادهسازیهای async I/O در Node است. اگر I/O را با worker-thread پیادهسازی کنید، در نهایت به عملکرد نامناسب، هدر دادن منابع و نوشتن کد اضافی منجر می شود.
مدیریت خطاها در استخرهای worker-thread ممکن است چالش برانگیز باشد زیرا همیشه یک پیوند واضح بین یک رویداد، worker-thread ای که توسط آن مدیریت می شود و اثری که ایجاد می شود وجود ندارد. تلاش برای دیباگ آنچه در حال رخ دادن است با استفاده از دستورات console.log() خسته کننده و مستعد ایجاد خطاهای دیگر است.
میتوانید با ضمیمه کردن یک AsyncResource به استخر، اطلاعات مفیدتری تولید کنید. این اطلاعات امکان بررسی کامل را فراهم می کند که اتفاقات داخل استخر را بررسی کنید و توالی کامل فعالیت هایی را که منجر به وقوع یک خطا می شود را مشاهده کنید.
اشتراک گذاری حافظه با استفاده از SharedArrayBuffer نیز ممکن است باعث بروز برخی مشکلات شود. شما باید ازویژگی atomics استفاده کنید یا سیستم مدیریت همزمانی خود را برای جلوگیری از race condition هنگام دسترسی و تغییر حافظه مشترک پیاده سازی کنید. اگر race condition اتفاق بیفتد، می توانند شرایط عجیبی را در برنامه شما ایجاد کند، و اغلب به سختی قابل شناسایی هستند، به خصوص زمانی که به حافظه مربوط می شود که در مکان های مختلف استفاده می شود.
استفاده از worker thread ها این امکان را به توسعه دهندگان Node.js می دهد که با شروع پردازش های جدید، کد را به صورت موازی اجرا کنند. این فرآیند یک multithreading واقعی نیست، با این حال هر thread یک فرآیند مستقل است که دسترسی به thread والد خود را ندارد. ارتباط بین thread ها تنها با استفاده از Alocated Shared Memory و یا تبادل داده از طریق رویداد امکان پذیر است.
ماژول worker_threads هنوز هم بخشی ارزشمند از اکوسیستم Node.js است. هیچ راه دیگری برای دستیابی به پردازش multithreading و موازی در مواقعی که از جاوا اسکریپت استفاده می کنیم وجود ندارد. با این حال، مهم است که محدودیتهای worker thread ها را بشناسید تا بتوانید در مورد موقعیت های استفاده از آنها بهترین تصمیم را داشته باشید. استفاده کردن از worker thread در موقعیت نامناسب می تواند عملکرد برنامه شما را کاهش دهد و استفاده از منابع را افزایش دهد.