การใช้งาน `SharedArrayBuffer` ใน TypeScript

การใช้งาน `SharedArrayBuffer` ใน TypeScript

บทความนี้อธิบายเกี่ยวกับ SharedArrayBuffer ใน TypeScript

เราจะอธิบาย SharedArrayBuffer ใน TypeScript พร้อมตัวอย่างการใช้งานจริง

YouTube Video

การใช้งาน SharedArrayBuffer ใน TypeScript

SharedArrayBuffer เป็นกลไกสำหรับการแบ่งปันพื้นที่หน่วยความจำเดียวกันระหว่างเธรดหลายตัว เช่น Web Workers โดยการใช้งานร่วมกับ Atomics คุณสามารถจัดการกับ data race และดำเนินการกับ shared memory ได้อย่างรวดเร็วและปลอดภัย

ข้อกำหนดเบื้องต้นและหมายเหตุ

เมื่อใช้ SharedArrayBuffer ในเบราว์เซอร์ คุณจำเป็นต้องตั้งค่า header COOP และ COEP เพื่อให้เป็นไปตามข้อกำหนด้านความปลอดภัยที่เรียกว่า cross-origin isolation ใน Node.js สามารถจัดการ shared memory ได้ง่ายโดยใช้ worker_threads

แนวคิดพื้นฐาน

SharedArrayBuffer คืออ็อบเจกต์ที่แทนชุด byte ขนาดคงที่ และคุณสามารถอ่าน/เขียนค่าต่างๆ โดยใช้ TypedArray หรือวิวอื่นที่คล้ายกัน การอ่าน/เขียนโดยตรงจะไม่ได้รับการซิงโครไนซ์ ดังนั้นคุณต้องใช้ API Atomics เพื่อให้มั่นใจว่าเป็นการดำเนินการแบบ atomic และใช้กลไก wait กับ notify เพื่อประสานการทำงาน

ตัวนับอย่างง่าย (เวอร์ชันเบราว์เซอร์)

ในตัวอย่างนี้ main thread จะสร้าง SharedArrayBuffer และส่งไปยัง Web Worker โดยทั้งสองจะเพิ่มค่าตัวนับร่วมกัน นี่คือตัวอย่างรูปแบบพื้นฐาน: การเพิ่มแบบ atomic ด้วย 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};
  • ในโค้ดนี้ main thread จะใช้ Atomics.add เพื่อเพิ่มค่าตัวเลขแบบ atomic ในฝั่ง worker.ts ก็สามารถเข้าถึงและแก้ไข SharedArrayBuffer เดียวกันได้

worker.ts (Worker ของเบราว์เซอร์)

นี่คือตัวอย่างที่ worker ได้รับ shared 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 และสามารถอัปเดตข้อมูลได้อย่างปลอดภัยโดยไม่มี race condition ด้วย Atomics

การซิงโครไนซ์ด้วย wait/notify

โดยใช้ Atomics.wait และ Atomics.notify คุณสามารถระงับการทำงานของเธรดจนกว่าจะมีเงื่อนไขที่กำหนด เกิดขึ้น เป็นการซิงโครไนซ์แบบ event-driven ระหว่าง worker ในเบราว์เซอร์ วิธีที่ปลอดภัยที่สุดคือใช้ Atomics.wait ภายใน Worker เท่านั้น

producer.ts (Worker ผลิตข้อมูลในเบราว์เซอร์)

Producer จะเขียนข้อมูลและแจ้งเตือนไปยัง Consumer ด้วย 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 ผู้รับข้อมูลในเบราว์เซอร์)

Consumer จะรอคอยด้วย Atomics.wait และกลับมาทำงานต่อเมื่อได้รับสัญญาณจาก producer

 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};
  • ในรูปแบบนี้ producer จะส่งสัญญาณแจ้งเตือนไปยัง consumer ผ่าน Atomics.notify และ consumer จะรออย่างมีประสิทธิภาพด้วย Atomics.wait Atomics.wait ไม่สามารถถูกเรียกใน main thread ของเบราว์เซอร์ได้ เพื่อป้องกัน UI ค้าง Atomics.wait จึงถูกจำกัดให้ใช้ภายใน Worker เท่านั้น

ตัวอย่างการใช้งานจริงกับ Node.js (worker_threads)

ใน Node.js คุณสามารถใช้งาน SharedArrayBuffer ได้โดยง่ายผ่าน worker_threads ด้านล่างคือตัวอย่าง TypeScript ที่มี type สำหรับ 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 เหมือนในเบราว์เซอร์ คุณจึงสามารถใช้ shared memory ได้ง่ายดายด้วย worker_threads เมื่อใช้ TypeScript ควรตรวจสอบให้แน่ชัดว่าคุณกำลัง build ด้วย CommonJS หรือ ESM

ข้อควรรู้เกี่ยวกับ TypeScript typing

SharedArrayBuffer และ Atomics ถูกนิยาม type มาในไลบรารีมาตรฐานแล้ว คุณจึงนำมาใช้ใน TypeScript ได้โดยตรง เมื่อส่งข้อความกับ Worker ควรนิยาม interface หรือ type ให้ชัดเจนเพื่อความปลอดภัย

1// Example: typed message
2type WorkerMessage = { type: 'init'; sab: SharedArrayBuffer } | { type: 'ping' };
  • การกำหนด type อย่างชัดเจน จะช่วยให้การใช้ postMessage กับ onmessage ปลอดภัยและสามารถตรวจสอบ type ได้

ตัวอย่างการใช้งานที่เหมาะสม

SharedArrayBuffer ไม่จำเป็นต้องใช้เสมอไป แต่มีประสิทธิภาพมากในสถานการณ์ที่ต้องการข้อได้เปรียบด้านความเร็วของหน่วยความจำที่ใช้ร่วมกัน การเข้าใจสถานการณ์ที่มันมีประสิทธิภาพจะนำไปสู่การเลือกใช้เทคโนโลยีที่เหมาะสม

  • เหมาะสำหรับการประมวลผลที่ต้องการความหน่วงต่ำ เช่น ประมวลผลเสียง/วิดีโอแบบเรียลไทม์ หรือฟิสิกส์ของเกม
  • หากเป็นแค่การแลกเปลี่ยนข้อมูลหรือส่งข้อมูลจำนวนมาก Transferable ArrayBuffer หรือ postMessage อาจใช้ง่ายกว่า SharedArrayBuffer

ข้อจำกัดและความปลอดภัย

การใช้ SharedArrayBuffer ในเบราว์เซอร์จำเป็นต้องมี cross-origin isolation: กำหนด COOP เป็น same-origin-allow-popups และ COEP เป็น require-corp SharedArrayBuffer จะไม่สามารถใช้งานได้หากไม่ได้ตั้งค่าตามที่ระบุ

เคล็ดลับด้านประสิทธิภาพและการดีบัก

การดำเนินการแบบ atomic (Atomics) นั้นรวดเร็ว แต่การ wait หรือซิงโครไนซ์บ่อยๆ อาจเพิ่มความหน่วง

ควรตรวจสอบประเด็นต่อไปนี้เพื่อจัดการ shared memory อย่างปลอดภัยและมีประสิทธิภาพ

  • การใช้งาน view เช่น Int32Array ต้องจัด byte alignment ให้ถูกต้อง
  • ควรระบุให้ชัดเจนว่า index ใน shared buffer ใดถูกใช้กับ process ไหน และรักษากติกานี้ให้คงที่ในโค้ด
  • หากคุณใช้ SharedArrayBuffer เหมือนกับ ArrayBuffer ธรรมดา อาจเกิด data race ได้ การเขียนไปยัง index เดียวกันจากหลายเธรด โดยไม่ใช้ Atomics ถือว่าอันตราย

สรุป

ด้วยการใช้ SharedArrayBuffer และ Atomics อย่างเหมาะสม คุณจะสามารถดำเนินการ shared memory ได้อย่างปลอดภัยและรวดเร็วใน TypeScript เริ่มต้นด้วยรูปแบบการซิงโครไนซ์ง่ายๆ เช่น ตัวนับหรือสัญญาณ จะเข้าใจได้ง่ายกว่า และถ้าบริหารการซิงโครไนซ์กับ index ได้ถูกต้อง จะสามารถใช้งานได้ดีแม้ในสถานการณ์ที่เน้นความหน่วงต่ำ

คุณสามารถติดตามบทความข้างต้นโดยใช้ Visual Studio Code บนช่อง YouTube ของเรา กรุณาตรวจสอบช่อง YouTube ด้วย

YouTube Video