TypeScriptにおける`SharedArrayBuffer`

TypeScriptにおける`SharedArrayBuffer`

この記事ではTypeScriptにおけるSharedArrayBufferについて説明します。

TypeScriptにおけるSharedArrayBufferについて実際的なサンプルを含めて解説します。

YouTube Video

TypeScriptにおけるSharedArrayBuffer

SharedArrayBufferWeb Workerなどの複数スレッドで同じメモリ領域を共有するための仕組みです。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 を使うことで、条件が整うまでスレッドを停止させることができ、ワーカースレッド内でイベント駆動の同期を実現できます。ブラウザでは Atomics.wait を Worker の内部で使用する方法が最も安全です。

producer.ts(ブラウザの Producer Worker)

Producer がデータを書き込み、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(ブラウザの Consumer Worker)

Consumer は 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.waitWorker の内部でのみ使用できるように制限されています。

Node.js(worker_threads)での実践例

Node.js 環境では worker_threads を使えば SharedArrayBuffer と同等な 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_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 と lib の型定義に含まれているため、そのまま TypeScript で利用できます。Worker とメッセージをやり取りする場合は、インターフェースを定義して型を明示しておくと安全です。

1// Example: typed message
2type WorkerMessage = { type: 'init'; sab: SharedArrayBuffer } | { type: 'ping' };
  • 明示的に型を定義しておくと、postMessageonmessageの処理が安全になり、型チェックが効きます。

実用的な使いどころ

SharedArrayBuffer は常に必要なわけではありませんが、共有メモリならではの高速性が求められる場面では大きな効果を発揮します。どのような状況で有効かを理解しておくことが、適切な技術選択につながります。

  • 高速に動作する共有バッファを必要とする低レイテンシの処理に向いており、音声や映像のリアルタイム処理やゲームの物理演算などで活用できます。
  • 単純なデータ受け渡しや大量データの移動では、SharedArrayBufferよりもTransferable ArrayBufferpostMessageの方が簡単な場合があります。

制約とセキュリティ

ブラウザで SharedArrayBuffer を使うにはクロスオリジン分離が必要で、COOP は same-origin-allow-popups、COEP は require-corp を指定します。これらがそろっていない場合は SharedArrayBuffer は無効になります。

パフォーマンスとデバッグのヒント

原子操作(Atomics)は高速ですが、頻繁な待ち合わせや過度の同期はレイテンシを悪化させます。

共有メモリを安全かつ効率的に扱うために、確認しておきたいポイントとして以下のような点があります。

  • Int32Array などのビューは、正しいバイトアライメントを守って扱います。
  • 同じ共有バッファのどのインデックスをどの処理が使用するのかを明確にし、コード上で一貫した合意を維持します。
  • SharedArrayBuffer を普通の ArrayBuffer のように扱うとデータ競合が生じます。Atomics を使わずに複数スレッドから同一インデックスを書き込むのは危険です。

まとめ

SharedArrayBufferAtomics を適切に活用することで、TypeScript でも安全で高速な共有メモリ処理を実現できます。まずはカウンタやシグナルといった小さな同期パターンから始めると理解しやすく、正しい同期とインデックス管理を徹底することで、低レイテンシが求められる場面でも効果を発揮します。

YouTubeチャンネルでは、Visual Studio Codeを用いて上記の記事を見ながら確認できます。 ぜひYouTubeチャンネルもご覧ください。

YouTube Video