توسعه فرانت‌اند

Web Workers و Multi-threading در JavaScript

راهنمای Worker در JavaScript با قرارداد پیام، Copy و Transfer، خطا و چرخه عمر؛ نمونه کامل محاسبه با صفحه، Worker و روش اجرای محلی.

1404/11/04 3 دقیقه مطالعه 2,420 بازدید

Web Worker اجرای یک کار را از محیط اصلی صفحه جدا می‌کند تا محاسبه طولانی فرصت تعامل را کمتر اشغال کند. این کار به معنی سریع‌تر شدن خودکار همه برنامه یا تبدیل همه کد JavaScript به پردازش موازی نیست. ابتدا باید معلوم باشد چه کاری زمان می‌گیرد و کدام بخش آن از DOM مستقل است. پردازش یک مجموعه عدد، تبدیل داده یا تحلیل فایل می‌تواند گزینه مناسبی باشد؛ تغییر متن یک دکمه همچنان مسئولیت محیط اصلی صفحه است.

برای مثال فرضی، کاربر یک گزارش بزرگ را مرتب می‌کند و هم‌زمان می‌خواهد فیلترها را تغییر دهد. اگر مرتب‌سازی طولانی در همان مسیر اجرای تعامل باشد، صفحه ممکن است دیر پاسخ دهد. انتقال بخش محاسباتی به Worker یکی از گزینه‌ها است. کاهش حجم داده، حذف محاسبه تکراری و تقسیم کار هم باید بررسی شوند. Worker هزینه ایجاد و تبادل پیام دارد؛ برای یک جمع ساده روی چند عدد، این هزینه ممکن است از خود کار بیشتر باشد.

محیط Worker با صفحه فرق دارد

راهنمای Web Workers در MDN اجرای اسکریپت در زمینه جدا و ارتباط پیام‌محور را توضیح می‌دهد. Worker به DOM صفحه دسترسی مستقیم ندارد و window محیط صفحه را در اختیارش نمی‌گیرد. بنابراین تابعی که document را تغییر می‌دهد نمی‌تواند بدون بازطراحی همان‌جا اجرا شود. کار را به ورودی داده، محاسبه مستقل و خروجی تقسیم کنید؛ صفحه ورودی را می‌فرستد و نتیجه را در رابط نشان می‌دهد.

مرز مستقل کمک می‌کند وابستگی پنهان را ببینید. تابع گزارش نباید برای فهم تنظیمات، مقدار یک input را از DOM بخواند؛ تنظیمات باید در پیام بیاید. خروجی هم بهتر است داده باشد، نه تکه HTML برای اجرای مستقیم. در مثال، Worker تعداد اعداد اول را حساب می‌کند و صفحه فقط پیام نتیجه را می‌نویسد. این طراحی قرارداد را روشن می‌کند و امکان استفاده دوباره از محاسبه را بیشتر می‌کند، حتی اگر در آینده Worker حذف یا روش اجرای آن تغییر کند.

پیام را مانند قرارداد API طراحی کنید

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

داده ورودی را در Worker هم بررسی کنید. قرار داشتن کد در همان پروژه، معتبر بودن همه پیام‌ها را تضمین نمی‌کند؛ تغییر بعدی یا اشتباه فراخواننده ممکن است قرارداد را بشکند. حد اندازه کار را تعریف کنید تا مقدار بسیار بزرگ منابع دستگاه را مصرف نکند. متن خطا باید به صفحه برگردد و در رابط قابل فهم نمایش داده شود. این مرز همان اصل بررسی داده است که مقاله الگوهای TypeScript با unknown و مدل داخلی توضیح می‌دهد.

Copy و Transfer یکسان نیستند

ارسال بسیاری از داده‌ها با Structured Clone انجام می‌شود و هزینه کپی می‌تواند در داده بزرگ مهم باشد. تابع و عنصر DOM را نمی‌توان مانند یک object ساده برای کار در محیط دیگر فرستاد. راهنمای Transferable Objects تفاوت انتقال مالکیت منابعی مانند ArrayBuffer را توضیح می‌دهد. پس از انتقال Buffer، فرستنده نباید فرض کند همچنان همان حافظه را برای استفاده معمول در اختیار دارد. این تصمیم باید در قرارداد داده دیده شود.

Transfer برای همه objectها وجود ندارد و یک آرایه معمولی را خودکار به حافظه مشترک تبدیل نمی‌کند. اگر خروجی کوچک است، افزودن این پیچیدگی شاید لازم نباشد. در نمونه اعداد اول، فقط یک عدد و پیام‌های کوتاه ردوبدل می‌شوند. برای داده حجیم، هزینه ساخت داده، ارسال، پردازش و دریافت را جدا ببینید. انتقال محاسبه به Worker در حالی که ساخت ورودی بزرگ همچنان روی صفحه انجام می‌شود ممکن است بخشی از کندی را باقی بگذارد.

نمونه کامل با شروع، پیشرفت و لغو

دو فایل زیر را با نام‌های index.html و prime-worker.js در یک پوشه قرار دهید. نمونه وابستگی npm ندارد. برای اجرای Worker از یک سرور محلی استفاده کنید؛ باز کردن با file ممکن است با محدودیت Origin روبه‌رو شود. اگر Python نصب دارید، داخل همان پوشه فرمان python -m http.server 8000 را اجرا و نشانی http://localhost:8000 را باز کنید. هیچ داده‌ای به سرویس بیرونی ارسال نمی‌شود. این محاسبه آموزشی برای سنجش سرعت همه دستگاه‌ها یا ساخت Benchmark عمومی طراحی نشده است.

<!doctype html><html lang="fa" dir="rtl"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>محاسبه با Worker</title><style>body{font:1rem/1.8 Tahoma,sans-serif;max-width:40rem;margin:2rem auto;padding:1rem}input,button{font:inherit;padding:.5rem;margin:.3rem}button:focus-visible,input:focus-visible{outline:3px solid #111;outline-offset:3px}</style></head><body>
<h1>شمارش اعداد اول</h1><label for="limit">حد عدد، از ۲ تا یک میلیون</label><input id="limit" type="number" min="2" max="1000000" value="100000" step="1"><p><button id="start" type="button">شروع</button><button id="cancel" type="button" disabled>لغو</button><button id="tap" type="button">کلیک مستقل: ۰</button></p><p id="status" role="status">آماده</p>
<script>
const start=document.getElementById('start'),cancel=document.getElementById('cancel'),status=document.getElementById('status'),limit=document.getElementById('limit');
let worker=null,taps=0;
document.getElementById('tap').onclick=e=>{e.currentTarget.textContent='کلیک مستقل: '+(++taps)};
function finish(text){if(worker)worker.terminate();worker=null;start.disabled=false;cancel.disabled=true;status.textContent=text}
start.onclick=()=>{
  if(!limit.reportValidity())return;
  if(!('Worker' in window)){status.textContent='این مرورگر Worker را پشتیبانی نمی‌کند.';return}
  try{worker=new Worker('./prime-worker.js')}catch{finish('ساخت Worker ممکن نشد.');return}
  start.disabled=true;cancel.disabled=false;status.textContent='در حال محاسبه';
  worker.onmessage=({data})=>{if(data.type==='progress')status.textContent='بررسی تا عدد '+data.checked+' از '+data.limit;else if(data.type==='done')finish('تعداد اعداد اول: '+data.count);else if(data.type==='error')finish(data.message)};
  worker.onerror=()=>finish('خطای اجرای Worker؛ مسیر فایل و تنظیمات را بررسی کنید.');
  worker.onmessageerror=()=>finish('پیام Worker قابل دریافت نبود.');
  worker.postMessage({type:'start',limit:Number(limit.value)});
};
cancel.onclick=()=>finish('محاسبه لغو شد؛ نتیجه نهایی تولید نشد.');
window.addEventListener('pagehide',()=>{if(worker)worker.terminate()});
</script></body></html>

فایل Worker تعداد اعداد اول تا حد واردشده را بررسی می‌کند. الگوریتم ساده آموزشی است و ادعای سریع‌ترین روش ندارد. حد ورودی بین ۲ و یک میلیون تعریف شده و پیام پیشرفت هر ده هزار عدد ارسال می‌شود. این بازه فقط اندازه نمونه است و معیار حجم مجاز محصول واقعی نیست. صفحه دکمه شروع را هنگام کار غیرفعال و لغو را فعال می‌کند؛ با لغو، Worker terminate می‌شود و نتیجه ناقص به عنوان نتیجه نهایی نمایش داده نمی‌شود.

self.onmessage=({data})=>{
  const limit=data?.limit;
  if(data?.type!=='start'||!Number.isSafeInteger(limit)||limit<2||limit>1000000){self.postMessage({type:'error',message:'حد عدد باید عدد صحیح بین ۲ و یک میلیون باشد.'});return}
  let count=0;
  for(let n=2;n<=limit;n++){
    let prime=true;
    for(let divisor=2;divisor*divisor<=n;divisor++){if(n%divisor===0){prime=false;break}}
    if(prime)count++;
    if(n%10000===0)self.postMessage({type:'progress',checked:n,limit});
  }
  self.postMessage({type:'done',count});
};

هر شروع یک Worker تازه می‌سازد و finish پس از پایان یا خطا آن را آزاد می‌کند. خروج از صفحه نیز Worker جاری را متوقف می‌کند. پیشرفت با متن status به رابط می‌رسد و دکمه شمارنده نشان می‌دهد یک تعامل ساده صفحه می‌تواند مستقل از محاسبه ادامه داشته باشد. تغییر مقدار input هنگام اجرا روی کار جاری اثر ندارد، چون حد در پیام شروع ثبت شده است. برای کار تازه باید محاسبه قبلی تمام یا لغو شود.

لغو و خطا را از ابتدا مشخص کنید

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

خطای محاسبه، خطای دریافت فایل Worker و پیام نامعتبر سه حالت متفاوت هستند. رابط باید کاربر را در وضعیت «در حال پردازش» رها نکند. دکمه‌ها پس از شکست به حالت قابل تلاش برگردند و نتیجه قدیمی با نتیجه تازه اشتباه نشود. CSP و مسیر فایل هم ممکن است روی ایجاد Worker اثر بگذارند. در پروژه دارای Bundler، روش معرفی URL فایل باید با ابزار هماهنگ باشد؛ کپی مسیر نسبی یک نمونه ساده در ساختار دیگری الزاماً فایل درست را پیدا نمی‌کند.

Dedicated، Shared و Service Worker را تفکیک کنید

نمونه از Dedicated Worker استفاده می‌کند که برای همان ایجادکننده کار می‌کند. SharedWorker می‌تواند میان زمینه‌های سازگار مشترک باشد و ارتباط آن با Port انجام می‌شود. نیاز به اشتراک کار میان تب‌ها، چرخه عمر و پشتیبانی مرورگر باید پیش از انتخاب آن بررسی شوند. داشتن چند تب به خودی خود دلیل استفاده از SharedWorker نیست؛ هزینه هماهنگی داده و قطع اتصال می‌تواند از صرفه‌جویی محاسبه بیشتر باشد.

Service Worker چرخه عمر رویدادمحور دارد و برای نقش‌هایی مانند مدیریت درخواست شبکه و تجربه آفلاین به کار می‌رود. آن را یک Worker محاسباتی همیشه‌روشن فرض نکنید. مرورگر می‌تواند محیط آن را متوقف و برای رویداد بعدی دوباره ایجاد کند. نگه داشتن وضعیت مهم فقط در متغیرهای آن قابل اتکا نیست. نام مشترک Worker نباید باعث شود مسئولیت Cache، محاسبه سنگین و ارتباط چند تب در یک معماری مبهم جمع شوند.

نتیجه را با هزینه کل ارزیابی کنید

Worker منابع CPU و حافظه همان دستگاه را مصرف می‌کند. ایجاد Worker برای هر ردیف گزارش ممکن است رقابت منابع و هزینه بیشتر ایجاد کند. برای کارهای زیاد، صف و تعداد محدود Workerها را بررسی کنید. تعداد هسته گزارش‌شده دستگاه هم دستور قطعی برای تعداد Worker مناسب نیست. پاسخ‌گویی رابط، مدت کل کار و مصرف منابع را با هدف محصول بسنجید. اتصال کند یا انتظار شبکه با انتقال یک تابع محاسباتی حل نمی‌شود.

در تحویل، قرارداد پیام، حد ورودی، رفتار لغو و روش آزادسازی را ثبت کنید. مقاله عملکرد React برای تفکیک هزینه رندر از محاسبه و Python و JavaScript در وب برای تعیین محل اجرای کار مفید هستند. تصمیم عملی این است که ابتدا یک کار مستقل و قابل اندازه‌گیری را انتخاب کنید، هزینه رفت‌وبرگشت آن را بشناسید و وضعیت‌های موفق و ناموفق را همراه نتیجه محاسبه طراحی کنید.

واکنش به این مقاله

اشتراک‌گذاری این مقاله:
دیدگاه‌ها 0
۰ / ۲۰۰۰

هنوز دیدگاهی ثبت نشده

اولین نفری باشید که نظر می‌دهد!