การใช้งาน `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.waitAtomics.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 ด้วย