مقدمه

همانطور که می‌دانید، جاوااسکریپت به طور سنتی یک زبان تک-ریسمانی (single-threaded) است. با معرفی Web Workers، امکان اجرای اسکریپت‌ها در ترِدهای پس‌زمینه فراهم شد تا از قفل شدن رابط کاربری در کارهای سنگین جلوگیری شود. با این حال، ارتباط بین ریسمان یا ترِد اصلی و یک Worker به طور پیش‌فرض از طریق کپی کردن داده‌ها (با متد postMessage()) انجام می‌شود که برای حجم بالای داده کارآمد نیست.

برای حل این مشکل، `SharedArrayBuffer` معرفی شد تا بتوان یک قطعه از حافظه را بین چند ترِد به اشتراک گذاشت. اما این کار چالش جدیدی به نام «وضعیت رقابتی» یا Race Condition را ایجاد می‌کند: وقتی دو یا چند ترِد سعی می‌کنند به صورت همزمان یک داده مشترک را بخوانند و بنویسند، نتیجه نهایی به زمان‌بندی غیرقابل پیش‌بینی اجرای آنها بستگی خواهد داشت و می‌تواند منجر به داده‌های خراب و نتایج اشتباه شود. اینجاست که `Atomics` وارد عمل می‌شود.

حافظه اشتراکی با SharedArrayBuffer

یک SharedArrayBuffer (SAB) یک شیء با اندازه ثابت است که یک بافر از بایت‌های باینری را نشان می‌دهد و می‌توان آن را بین ترِدهای مختلف (مثلاً ترِد اصلی و یک Worker) به اشتراک گذاشت. برخلاف ArrayBuffer معمولی، وقتی یک SharedArrayBuffe را به یک Worker ارسال می‌کنید، به جای کپی شدن، یک ارجاع به همان بلوک حافظه ارسال می‌شود. این یعنی هر تغییری که یک ترِد روی این حافظه اعمال کند، فوراً برای ترِد دیگر قابل مشاهده است.

نکته امنیتی مهم: به دلیل آسیب‌پذیری‌های امنیتی (مانند Spectre)، استفاده از SharedArrayBuffer نیازمند این است که صفحه شما در یک «بستر امن» (Secure Context) باشد. این یعنی وب‌سایت باید روی HTTPS سرویس‌دهی شود و سرور باید هدرهای خاصی را تنظیم کند: Cross-Origin-Embedder-Policy: require-corp و Cross-Origin-Opener-Policy: same-origin. بدون این هدرها، مرورگرها اجازه استفاده از SharedArrayBuffer را نخواهند داد.

عملیات ایمن با Atomics

Atomics یک شیء سراسری است که مجموعه‌ای از متدهای استاتیک را برای انجام عملیات اتمیک و ایمن روی یک SharedArrayBuffer فراهم می‌کند. عملیات "اتمیک" به این معنی است که یک عملیات (مانند خواندن، نوشتن یا جمع) به صورت یک واحد کامل و غیرقابل تقسیم اجرا می‌شود. این تضمین می‌کند که هیچ ترِد دیگری نمی‌تواند در وسط اجرای این عملیات، داده را تغییر دهد و از بروز Race Condition جلوگیری می‌کند.

عملیات پایه اتمیک

متدهای Atomics همیشه یک TypedArray (مانند Int32Array) که روی یک SharedArrayBuffer ساخته شده را به عنوان آرگومان اول، و ایندکس خانه‌ی مورد نظر را به عنوان آرگومان دوم می‌گیرند.

  • Atomics.add(typedArray, index, value): مقدار value را به مقدار خانه index اضافه کرده و مقدار قبلی آن خانه را برمی‌گرداند.
  • Atomics.sub(typedArray, index, value): مشابه `add` عمل تفریق را انجام می‌دهد.
  • Atomics.load(typedArray, index): مقدار خانه index را به صورت اتمیک می‌خواند.
  • Atomics.store(typedArray, index, value): مقدار value را به صورت اتمیک در خانه index ذخیره می‌کند.
Copy Icon JAVASCRIPT
// In a worker file (worker.js)
self.onmessage = function({ data: sharedArray }) {
    console.log('Worker received shared array.');
    
    // Safely increment the value at index 0
    Atomics.add(sharedArray, 0, 1);
    
    console.log('Worker finished incrementing.');
};

در کد بالا که مربوط به یک Worker است، به جای sharedArray[0]++ که یک عملیات غیرایمن است، از Atomics.add() استفاده می‌کنیم. این کار تضمین می‌کند که حتی اگر چندین Worker همزمان این کد را اجرا کنند، هیچ به‌روزرسانی‌ای از بین نمی‌رود.

هماهنگی بین تردها با Wait و Notify

Atomics همچنین ابزارهایی برای هماهنگ کردن اجرای ترِدها فراهم می‌کند. این کار به ما اجازه می‌دهد یک ترِد را به حالت تعلیق درآوریم تا زمانی که یک رویداد خاص در ترِد دیگری رخ دهد.

  • Atomics.wait(typedArray, index, value): ترِد فعلی را به حالت خواب (sleep) می‌برد اگر مقدار خانه index برابر با value باشد. ترِد منتظر می‌ماند تا توسط notify بیدار شود.
  • Atomics.notify(typedArray, index, count): تعداد count از ترِدهایی را که روی خانه index منتظر هستند، بیدار می‌کند.
Copy Icon JAVASCRIPT
// worker.js
console.log('Worker waiting for signal...');
// Wait at index 1 if its value is 0
Atomics.wait(sharedArray, 1, 0);
console.log('Worker woken up! Starting task.');

// main.js
console.log('Main thread will send signal in 2s.');
setTimeout(() => {
    // Change the value at index 1 to signal the worker
    Atomics.store(sharedArray, 1, 1);
    // Notify any workers waiting on index 1
    Atomics.notify(sharedArray, 1);
}, 2000);

در این الگو، Worker با استفاده از Atomics.wait() اجرای خود را متوقف می‌کند. ترِد اصلی پس از ۲ ثانیه، مقدار را تغییر داده و با Atomics.notify() به Worker سیگنال می‌دهد تا به کار خود ادامه دهد. این روش بسیار بهینه‌تر از یک حلقه while برای بررسی مداوم یک مقدار است.

در این درس با مفاهیم پیشرفته پردازش موازی در جاوااسکریپت با استفاده از SharedArrayBuffer و Atomics آشنا شدیم. این ابزارها برای کاربردهای سنگین و محاسباتی که نیاز به کارایی بالا دارند، بسیار قدرتمند هستند. در درس بعدی، به سراغ یک API بسیار رایج‌تر و کاربردی‌تر در تعاملات روزمره خواهیم رفت: Clipboard API، که به ما امکان دسترسی به کلیپ‌بورد سیستم را می‌دهد.