ה-`SharedArrayBuffer` ב-TypeScript
מאמר זה מסביר את ה-SharedArrayBuffer ב-TypeScript.
נסביר את SharedArrayBuffer ב-TypeScript עם דוגמאות מעשיות.
YouTube Video
ה-SharedArrayBuffer ב-TypeScript
SharedArrayBuffer הוא מנגנון לשיתוף אותו מרחב זיכרון בין מספר תהליכונים (threads), כגון Web Workers. בשילוב עם Atomics, ניתן לנהל מרוצי נתונים (data races) ולבצע פעולות זיכרון משותף בעלות שיהוי נמוך.
דרישות מוקדמות והערות
בשימוש ב-SharedArrayBuffer בדפדפן יש צורך בכותרות COOP ו-COEP כדי לעמוד בדרישות אבטחה המכונות cross-origin isolation (בידוד מקורות). ב-Node.js ניתן לטפל בזיכרון משותף בקלות יחסית על ידי שימוש ב-worker_threads.
מושגים בסיסיים
SharedArrayBuffer הוא אובייקט המייצג רצף בתים באורך קבוע, וניתן לקרוא ולכתוב ערכים מספריים באמצעות TypedArray ותצוגות דומות. פעולות קריאה/כתיבה פשוטות אינן מסונכרנות, לכן יש להשתמש ב-API של Atomics לביצוע פעולות אטומיות, וכן במנגנונים של wait ו-notify לתיאום בין תהליכונים.
מונה פשוט (גרסת דפדפן)
בדוגמה זו, התהליכון הראשי יוצר SharedArrayBuffer ומעביר אותו ל-Web Worker, כאשר שניהם מגדילים מונה משותף. זה מדגים את הדפוס הבסיסי: חיבור אטומי בעזרת Atomics.add וקריאה בעזרת Atomics.load.
main.ts (צד הדפדפן)
דוגמה זו מראה כיצד התהליכון הראשי יוצר SharedArrayBuffer ומשתף אותו עם Worker לצורך גישה מרובת תהליכונים.
1// main.ts
2// Create a 4-byte (Int32) shared buffer for one counter
3const sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 1);
4const counter = new Int32Array(sab); // view over the buffer
5
6// Create worker and transfer the SharedArrayBuffer
7const worker = new Worker(new URL('./worker.ts', import.meta.url), { type: 'module' });
8worker.postMessage({ sab });
9
10// Increase counter in main thread every 200ms
11setInterval(() => {
12 const newVal = Atomics.add(counter, 0, 1) + 1; // Atomically increment
13 console.log('Main incremented ->', newVal);
14}, 200);
15
16// Listen for messages (optional)
17worker.onmessage = (e) => {
18 console.log('From worker:', e.data);
19};- בקוד זה, התהליכון הראשי משתמש ב-
Atomics.addעל מנת להגדיל את הערך בצורה אטומית. בצד שלworker.ts, אותוSharedArrayBufferניתן לגישה ולשינוי.
worker.ts (Worker בדפדפן)
זהו מקרה לדוגמה בו ה-Worker מקבל את אותו Buffer משותף ומבצע בו הפחתה או שינוי באופן מחזורי.
1// worker.ts
2self.onmessage = (ev: MessageEvent) => {
3 const { sab } = ev.data as { sab: SharedArrayBuffer };
4 const counter = new Int32Array(sab);
5
6 // Every 350ms, atomically add 2 (demonstration)
7 setInterval(() => {
8 const newVal = Atomics.add(counter, 0, 2) + 2;
9 // Optionally notify main thread (postMessage)
10 self.postMessage(`Worker added 2 -> ${newVal}`);
11 }, 350);
12};- ה-Worker משנה את אותו זיכרון באמצעות
Int32Array, ועדכונים מתבצעים ללא מרוצי נתונים בזכות השימוש ב-Atomics.
סינכרון באמצעות wait/notify
בעזרת Atomics.wait ו-Atomics.notify ניתן להשעות תהליכונים עד שמתקיימים תנאים מסוימים וכך לאפשר סנכרון מבוסס אירועים בתוך Workers. בדפדפנים, השיטה הבטוחה ביותר היא להשתמש ב-Atomics.wait בתוך Worker.
producer.ts (Worker המפיק בדפדפן)
המפיק כותב נתונים ומתריע (notifies) לצרכן באמצעות notify.
1// producer.ts (worker)
2self.onmessage = (ev: MessageEvent) => {
3 const { sab } = ev.data as { sab: SharedArrayBuffer };
4 const state = new Int32Array(sab); // state[0] will be 0=empty,1=filled
5
6 // produce every 500ms
7 setInterval(() => {
8 // produce: set state to 1 (filled)
9 Atomics.store(state, 0, 1);
10 Atomics.notify(state, 0, 1); // wake one waiter
11 self.postMessage('produced');
12 }, 500);
13};consumer.ts (Worker הצרכן בדפדפן)
הצרכן ממתין עם Atomics.wait ומחדש עיבוד כאשר מתקבלת התראה מהמפיק.
1// consumer.ts (worker)
2self.onmessage = (ev: MessageEvent) => {
3 const { sab } = ev.data as { sab: SharedArrayBuffer };
4 const state = new Int32Array(sab);
5
6 // consumer loop
7 (async function consumeLoop() {
8 while (true) {
9 // if state is 0 => wait until notified (value changes)
10 while (Atomics.load(state, 0) === 0) {
11 // Blocks this worker until notified or timeout
12 Atomics.wait(state, 0, 0);
13 }
14 // consume (reset to 0)
15 // (processing simulated)
16 // read data from another shared buffer in real scenarios
17 Atomics.store(state, 0, 0);
18 // continue loop
19 self.postMessage('consumed');
20 }
21 })();
22};- בדפוס זה, המפיק מתריע לצרכן בעזרת
Atomics.notifyוהצרכן ממתין באופן יעיל בעזרתAtomics.wait. לא ניתן לקרוא ל-Atomics.waitעל התהליכון הראשי בדפדפנים. כדי למנוע הקפאה של ממשק המשתמש, השימוש ב-Atomics.waitמוגבל לעבודה בתוך Workers בלבד.
דוגמה מעשית עם Node.js (worker_threads)
בסביבת Node.js ניתן לנהל את המימוש של SharedArrayBuffer בעזרת מודול worker_threads. להלן דוגמה טיפוסית (typed) ב-TypeScript עבור Node.js.
main-node.ts
התהליכון הראשי יוצר את ה-buffer ומעביר אותו ל-Worker.
1// main-node.ts
2import { Worker } from 'worker_threads';
3import path from 'path';
4
5// Create SharedArrayBuffer for one 32-bit integer
6const sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT);
7const counter = new Int32Array(sab);
8
9// Spawn worker and pass sab via workerData
10const worker = new Worker(path.resolve(__dirname, 'worker-node.js'), {
11 workerData: { sab }
12});
13
14// Log messages from worker
15worker.on('message', (msg) => console.log('Worker:', msg));
16
17// Increment in main thread periodically
18setInterval(() => {
19 const val = Atomics.add(counter, 0, 1) + 1;
20 console.log('Main increment ->', val);
21}, 300);worker-node.ts
בדוגמה זו, נעשה שימוש ב-parentPort ו-workerData מתוך worker_threads בצד של ה-Worker.
1// worker-node.ts
2import { parentPort, workerData } from 'worker_threads';
3
4const sab = workerData.sab as SharedArrayBuffer;
5const counter = new Int32Array(sab);
6
7// Periodically add 5
8setInterval(() => {
9 const val = Atomics.add(counter, 0, 5) + 5;
10 parentPort?.postMessage(`Added 5 -> ${val}`);
11}, 700);- ב-Node.js אין הגבלות COOP או COEP כמו בדפדפן, ולכן ניהול זיכרון משותף מתבצע בקלות רק בעזרת
worker_threads. בשימוש ב-TypeScript, שימו לב אם הפרויקט מוגדר כ-CommonJS או ESM.
נקודות טיפוס (Typing) ב-TypeScript
SharedArrayBuffer ו-Atomics כלולים בהגדרות הטיפוס התקניות של ה-DOM והספריות, ולכן ניתן להשתמש בהם ישירות ב-TypeScript. בעת העברת הודעות עם Workers, מומלץ להגדיר ממשקים ולציין טיפוסים בצורה ברורה.
1// Example: typed message
2type WorkerMessage = { type: 'init'; sab: SharedArrayBuffer } | { type: 'ping' };- הגדרת טיפוסים באופן מפורש משפרת את הבטיחות בטיפול ב-
postMessageו-onmessageומאפשרת בדיקת טיפוסים.
מקרי שימוש מעשיים
SharedArrayBuffer אינו נדרש תמיד, אך הוא יעיל במיוחד במצבים שבהם יש צורך ביתרונות המהירות של זיכרון משותף. הבנת המצבים שבהם הוא יעיל מובילה לבחירה טכנולוגית מתאימה.
- הפתרון מתאים לעיבוד בעל שיהוי נמוך הדורש buffers משותפים במהירות גבוהה, כמו בעיבוד שמע/וידאו בזמן אמת או פיזיקה במשחקים.
- להעברת נתונים פשוטה או העברת כמויות גדולות של נתונים, פעמים רבות יותר נוח להשתמש ב-
Transferable ArrayBufferאוpostMessageמאשר ב-SharedArrayBuffer.
הגבלות ואבטחה
להפעלת SharedArrayBuffer בדפדפן דרוש בידוד cross-origin: יש להגדיר COOP ל-same-origin-allow-popups ו-COEP ל-require-corp. SharedArrayBuffer יושבת אם דרישות אלו לא יתקיימו.
טיפים לביצועים וניפוי באגים
פעולות אטומיות (Atomics) מהירות, אך המתנה תדירה או סנכרון מוגזם עלולים להעלות את השיהוי.
הנקודות הבאות חשובות לטיפול בטוח ויעיל בזיכרון משותף:.
- יש להקפיד על יישור בתים (byte alignment) נכון לעבודה עם תצוגות כמו
Int32Array. - השאירו בהירות באשר לאילו אינדקסים של אותו buffer משותף משמשים כל תהליך, והקפידו על מוסכמות עקביות בקוד.
- אם מתייחסים ל-
SharedArrayBufferכאלArrayBufferרגיל עלולים להתרחש מרוצי נתונים. כתיבה לאותו אינדקס ממספר תהליכונים בלי שימוש ב-Atomicsמסוכנת.
סיכום
בשימוש נכון ב-SharedArrayBuffer ו-Atomics ניתן להגשים פעולות זיכרון משותף בטוחות ומהירות גם ב-TypeScript. התחלה עם דפוסי סנכרון פשוטים כגון מונים או אותות (signals) מקלה על ההבנה, וניהול נכון של סנכרון ואינדקסים מאפשר יעילות גם במצבי שיהוי נמוך.
תוכלו לעקוב אחר המאמר שלמעלה באמצעות Visual Studio Code בערוץ היוטיוב שלנו. נא לבדוק גם את ערוץ היוטיוב.