TypeScript 中的 `SharedArrayBuffer`

TypeScript 中的 `SharedArrayBuffer`

本文將解釋 TypeScript 中的 SharedArrayBuffer

我們將透過實際範例說明如何在 TypeScript 中使用 SharedArrayBuffer

YouTube Video

TypeScript 中的 SharedArrayBuffer

SharedArrayBuffer 是一種可以在多個執行緒之間(例如 Web Workers)共享相同記憶體空間的機制。結合 Atomics 使用時,可以管理資料競爭並實現低延遲的共享記憶體操作。

前置條件與注意事項

在瀏覽器中使用 SharedArrayBuffer 時,必須設定 COOP 與 COEP 標頭,以符合「跨來源隔離」的安全需求。在 Node.js 環境中,可以透過 worker_threads 較容易地處理共用記憶體。

基本概念

SharedArrayBuffer 是代表固定長度位元組序列的物件,可透過 TypedArray 等視圖進行數值讀寫。單純的讀寫操作並非同步,因此需使用 Atomics API 來確保原子性運算,並透過 waitnotify 等機制實現協調。

簡單計數器(瀏覽器版本)

在此範例中,主執行緒建立一個 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 如何接收相同的共享記憶體區塊,並定期遞減或操作其內容。

 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.waitAtomics.notify,您可以在特定條件成立前讓執行緒暫停,實現 Worker 執行緒之間的事件驅動式同步。在瀏覽器中,最安全的方式是在 Worker 內部使用 Atomics.wait

producer.ts(瀏覽器生產者 Worker)

生產者寫入資料後,利用 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 不能在瀏覽器的主執行緒中呼叫。為防止 UI 凍結,Atomics.wait 僅限在 Worker 內使用。

Node.js 實務範例(worker_threads)

在 Node.js 環境中,可以用 worker_threads 實現 SharedArrayBuffer 的功能。以下是一個 Node.js 環境下具型別的 TypeScript 範例。

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

在此範例中,Worker 端使用來自 worker_threadsparentPortworkerData

 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 設定。

TypeScript 型別重點

SharedArrayBufferAtomics 已內建於標準 DOM 與函式庫的型別定義中,因此可直接於 TypeScript 使用。與 Worker 進行訊息交換時,明確定義介面與型別會更安全。

1// Example: typed message
2type WorkerMessage = { type: 'init'; sab: SharedArrayBuffer } | { type: 'ping' };
  • 明確定義型別可讓 postMessageonmessage 處理更安全,並支援型別檢查。

實際應用範例

SharedArrayBuffer 並非總是必要,但在需要共享記憶體帶來速度優勢的情境下非常有效。了解其有效的應用情境可以幫助做出合適的技術選擇。

  • 它適合用於需要高速共用 buffer 的低延遲處理,例如即時音訊/影像處理或遊戲物理計算等。
  • 若僅為單純資料交換或大量資料傳輸,使用 Transferable ArrayBufferpostMessage 通常比 SharedArrayBuffer 更為簡便。

限制與安全性

於瀏覽器中使用 SharedArrayBuffer 時,必須啟用跨來源隔離:COOP 設為 same-origin-allow-popups,COEP 設為 require-corp。若未符合上述要求,SharedArrayBuffer 功能將會被禁用。

效能與除錯建議

雖然原子運算 (Atomics) 效率很高,但頻繁等待與過度同步會提升延遲。

為安全高效地操作共享記憶體,應注意下列幾點。

  • Int32Array 等視圖必須注意正確的位元組對齊(byte alignment)。
  • 同一個共享緩衝區的索引應清楚劃分給各個處理程序並在程式中維持一致慣例。
  • 如果把 SharedArrayBuffer 當作一般 ArrayBuffer 使用,將會產生資料競爭。未透過 Atomics,讓多執行緒寫入同一索引是非常危險的。

總結

善用 SharedArrayBufferAtomics,即使在 TypeScript 也能安全迅速地執行共用記憶體操作。從簡單的計數器或訊號等同步模式開始較易理解;只要妥善管理同步與索引,即使在低延遲情境下也能發揮效能。

您可以在我們的 YouTube 頻道上使用 Visual Studio Code 來跟隨上述文章一起學習。 請也查看我們的 YouTube 頻道。

YouTube Video