JavaScriptにおける`SharedArrayBuffer`

JavaScriptにおける`SharedArrayBuffer`

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

SharedArrayBuffer の基本から、使い方、具体的なユースケース、セキュリティ面での留意事項までを詳しく解説します。

YouTube Video

JavaScriptにおけるSharedArrayBuffer

SharedArrayBufferは、JavaScriptにおいて、複数のスレッド間でメモリを共有するための強力なツールです。特に、Web Workers との組み合わせによって並列処理を実現できるため、計算集約的なタスクやリアルタイム性が求められるアプリケーションで効果を発揮します。

SharedArrayBufferとは?

SharedArrayBuffer は、JavaScript で複数のスレッド(主に Web Workers)間でバイナリデータを共有できるメモリバッファを提供します。通常の ArrayBuffer はメインスレッドやワーカー間でコピーが必要ですが、SharedArrayBuffer はコピーすることなくメモリを直接共有できるため、パフォーマンスが大幅に向上します。

特徴

  • 共有メモリ 複数のスレッドで同じメモリ空間を扱うことができます。
  • パフォーマンス向上 コピーを省略できるため、大量のデータを処理する際のオーバーヘッドが削減されます。
  • スレッド間同期 メモリにアクセスする際の競合を避けるために、Atomics と組み合わせて同期処理を行うことができます。

基本的な使用例

 1// Create a 16-byte shared memory
 2const sharedBuffer = new SharedArrayBuffer(16);
 3
 4// Treat it as an Int32Array
 5const sharedArray = new Int32Array(sharedBuffer);
 6
 7// Set a value
 8sharedArray[0] = 42;
 9
10console.log(sharedArray[0]);  // 42

この例では、SharedArrayBuffer を使って 16 バイトのメモリ領域を作成し、そのメモリ領域を Int32Array として扱っています。複数のスレッド間でこのメモリバッファを共有することが可能です。

Web Workersとの併用

SharedArrayBuffer の真価は、Web Workers との併用によって発揮されます。以下のコードは、メインスレッドとワーカー間で共有メモリを使用する例です。

メインスレッド側

 1// Create a shared buffer
 2const sharedBuffer = new SharedArrayBuffer(16);
 3const sharedArray = new Int32Array(sharedBuffer);
 4
 5// Create a worker
 6const worker = new Worker('worker.js');
 7
 8// Pass the shared buffer to the worker
 9worker.postMessage(sharedBuffer);
10
11// Modify the memory
12// Output : Main thread: 100
13sharedArray[0] = 100;
14console.log("Main thread: ", sharedArray[0]);

ワーカー側 (worker.js)

 1// worker.js
 2self.onmessage = function(event) {
 3    // Use the received shared buffer
 4    const sharedArray = new Int32Array(event.data);
 5
 6    // Read the contents of the memory
 7    // Output : Worker thread: 100
 8    console.log("Worker thread: ", sharedArray[0]);
 9
10    // Change the value
11    sharedArray[0] = 200;
12};
  • この例では、メインスレッドが共有バッファを作成し、ワーカーに渡しています。ワーカーはこのバッファにアクセスして値を読み取り、書き換えることができます。このように、データのコピーをせずにスレッド間で共有することができます。

双方向の更新確認

SharedArrayBuffer を用いることで、メインスレッドとワーカーが互いに同じメモリを読み書きできるため、双方向の更新確認が可能です。以下は、メインスレッドが値を設定し、ワーカーが値を変更した後、メインスレッドがその変更を確認する例です。

メインスレッド側

 1// Create a shared buffer
 2const sharedBuffer = new SharedArrayBuffer(16);
 3const sharedArray = new Int32Array(sharedBuffer);
 4
 5// Create a worker
 6const worker = new Worker('worker.js');
 7
 8// Pass the shared buffer to the worker
 9worker.postMessage(sharedBuffer);
10
11// Set initial value
12// Output : Main thread initial: 100
13sharedArray[0] = 100;
14console.log("Main thread initial:", sharedArray[0]);
15
16// Listen for worker confirmation
17worker.onmessage = () => {
18    // Output : Main thread after worker update: 200
19    console.log("Main thread after worker update:", sharedArray[0]);
20};

ワーカー側 (worker.js)

 1// worker.js
 2self.onmessage = function(event) {
 3    const sharedArray = new Int32Array(event.data);
 4
 5    // Read initial value
 6    // Output : Worker thread received: 100
 7    console.log("Worker thread received:", sharedArray[0]);
 8
 9    // Update the value
10    sharedArray[0] = 200;
11
12    // Notify main thread
13    self.postMessage("Value updated");
14};
  • この例では、メインスレッドが最初に 100 を書き込み、ワーカーがそれを読み取ったあと 200 に書き換えます。その後、ワーカーがメインスレッドに通知を送り、メインスレッドは共有メモリを再度読み取って更新を確認します。このように、通知と組み合わせることで、双方向の更新確認が実現できます。

Atomicsを使った同期処理

共有メモリを使用する際に注意しなければならないのが、データ競合や不整合の問題です。複数のスレッドが同時に同じメモリにアクセスする場合、競合が発生し得ます。これを防ぐために、JavaScript では Atomics オブジェクトを使用して同期を行います。

例えば、複数のスレッドでカウンターを安全にインクリメントする場合、Atomics を使うことで、競合を防ぐことができます。

1const sharedBuffer = new SharedArrayBuffer(16);
2const sharedArray = new Int32Array(sharedBuffer);
3
4// Increment the counter
5Atomics.add(sharedArray, 0, 1);
6
7console.log(Atomics.load(sharedArray, 0));  // 1

Atomics.add は、特定のインデックスにある値を原子操作でインクリメントし、その後の値を返します。この操作は他のスレッドとの間で競合が発生しないように保証されています。Atomics.load もまた、共有メモリの値を安全に読み取るために使われます。

Atomics.waitAtomics.notify を使った待機と通知

SharedArrayBuffer を利用する際、あるワーカーが特定の条件を満たすまで待機し、別のワーカーがその条件を満たしたら通知する、といった処理が必要になる場合があります。このとき役立つのが Atomics.waitAtomics.notify です。

Atomics.wait は共有メモリ上の特定のインデックスの値が変化するまでスレッドをブロックし、Atomics.notify は待機しているスレッドに「進んでよい」という通知を送ります。これにより、複数のワーカー間で待機と通知を安全に行うことができます。ただし、Atomics.wait は メインスレッドでは使用できず、ワーカー内でのみ利用可能 です。

 1// Create a shared buffer (1 Int32 slot is enough for signaling)
 2const sharedBuffer = new SharedArrayBuffer(4);
 3const sharedArray = new Int32Array(sharedBuffer);
 4
 5// Create workers with names
 6const waiter = new Worker('worker.js', { name: 'waiter' });
 7const notifier = new Worker('worker.js', { name: 'notifier' });
 8
 9// Pass the shared buffer to both
10waiter.postMessage(sharedBuffer);
11notifier.postMessage(sharedBuffer);
12
13// Listen for messages
14waiter.onmessage = (event) => {
15    console.log(`[Main] Message from waiter:`, event.data);
16};
17notifier.onmessage = (event) => {
18    console.log(`[Main] Message from notifier:`, event.data);
19};
  • メインスレッドでは、共有メモリとして SharedArrayBuffer を作成し、それを 1 要素だけを持つ Int32Array に変換しています。この 1 つの整数スロットをシグナルとして利用し、ワーカー間の同期を行います。次に、2 つのワーカーを作成し、それぞれに name プロパティで waiter(待機役)と notifier(通知役)という役割を与えています。最後に、この共有バッファを両方のワーカーへ渡し、さらに onmessage ハンドラを設定することで、各ワーカーから送信されるメッセージを受け取れるようにしています。

ワーカー側 (worker.js)

 1// worker.js
 2onmessage = (event) => {
 3    const sharedArray = new Int32Array(event.data);
 4
 5    if (self.name === 'waiter') {
 6        postMessage('Waiter is waiting...');
 7        // Wait until notifier signals index 0
 8        Atomics.wait(sharedArray, 0, 0);
 9        postMessage('Waiter was notified!');
10    }
11
12    if (self.name === 'notifier') {
13        postMessage('Notifier is preparing...');
14        setTimeout(() => {
15            // Notify waiter after 2 seconds
16            Atomics.store(sharedArray, 0, 1);
17            Atomics.notify(sharedArray, 0, 1);
18            postMessage('Notifier has sent the signal!');
19        }, 2000);
20    }
21};
22// Output
23// [Main] Message from waiter: Waiter is waiting...
24// [Main] Message from notifier: Notifier is preparing...
25// [Main] Message from notifier: Notifier has sent the signal!
26// [Main] Message from waiter: Waiter was notified!
  • この例では、ワーカー waiterAtomics.wait によってインデックス 0 の値が 0 の間は待機状態になります。一方 ワーカー notifierAtomics.store で値を 123 に変更し、Atomics.notify を呼ぶと、ワーカー waiter が再開されて更新後の値を取得できます。これにより、効率的で安全なスレッド間の待機と通知が実現できます。

SharedArrayBuffer のユースケース

SharedArrayBuffer は、以下のようなユースケースで特に有効です。

  • リアルタイム処理 音声や映像の処理、ゲームのエンジンなど、低遅延が求められるアプリケーションで、スレッド間でデータを即座に共有する必要がある場合に向いています。
  • 並列計算 大量のデータを複数のスレッドで同時に処理する場合、SharedArrayBuffer を使うことでメモリのコピーを回避し、パフォーマンスを向上させることができます。
  • 機械学習 データの前処理や推論処理を並列化することで、効率的な計算が可能になります。

セキュリティ上の注意点

SharedArrayBuffer は強力な機能ですが、セキュリティ上のリスクも伴います。特に、Spectre と呼ばれるサイドチャネル攻撃に対する懸念から、一時的にサポートが停止されたこともあります。この脆弱性を克服するため、ブラウザは以下の対策を講じています。

  • サイト隔離 SharedArrayBuffer の使用を許可するサイトは、他のサイトと完全に隔離されたプロセスで実行されます。
  • クロスオリジンリソースポリシー SharedArrayBuffer を使用するには、Cross-Origin-Opener-Policy および Cross-Origin-Embedder-Policy ヘッダーを正しく設定する必要があります。

例えば、以下のようなヘッダーを設定することで、SharedArrayBuffer の使用が可能になります。

1Cross-Origin-Opener-Policy: same-origin
2Cross-Origin-Embedder-Policy: require-corp

これにより、外部のリソースが現在のコンテンツに干渉することを防ぎ、セキュリティを高めることができます。

まとめ

SharedArrayBuffer は、複数のスレッド間でメモリを共有するための非常に強力なツールです。パフォーマンスを向上させるために不可欠な技術であり、特にリアルタイム処理や並列計算の分野でその効果が発揮されます。ただし、セキュリティリスクも伴うため、正しい設定と同期処理が重要です。

SharedArrayBuffer を活用することで、より高度でパフォーマンスに優れた Web アプリケーションを構築できます。

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

YouTube Video