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

主线程创建缓冲区并传递给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_threads中的parentPortworkerData

 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 并非总是必需的,但在需要共享内存带来的速度优势时非常有效。了解其适用场景有助于做出合适的技术选择。

  • 适用于需要高速共享缓冲区的低延迟处理,如实时音视频处理或游戏物理计算。
  • 对于简单数据交换或大量数据传输,Transferable ArrayBufferpostMessage可能比SharedArrayBuffer更容易使用。

限制与安全性

在浏览器中使用SharedArrayBuffer时需要跨域隔离:COOP设置为same-origin-allow-popups,COEP设置为require-corp。如果不满足这些要求,SharedArrayBuffer将被禁用。

性能与调试建议

原子操作(Atomics)很快,但频繁的等待和过度同步会增加延迟。

为安全高效地处理共享内存,应注意以下事项。

  • Int32Array等视图应正确处理字节对齐。
  • 明确同一个共享缓冲的哪些索引由哪些进程使用,并在代码中保持一致规范。
  • 如果像普通ArrayBuffer一样对待SharedArrayBuffer,会发生数据竞争。未使用Atomics便从多个线程写入同一索引是很危险的。

总结

合理使用SharedArrayBufferAtomics,即使在TypeScript中也能实现安全快速的共享内存操作。从计数器、信号等简单同步模式入门较易理解,并且只要管理好正确的同步与索引,即使在低延迟场景下也能高效应对。

您可以在我们的YouTube频道上使用Visual Studio Code跟随上述文章进行学习。 请也查看我们的YouTube频道。

YouTube Video