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.wait
と Atomics.notify
を使った待機と通知
SharedArrayBuffer
を利用する際、あるワーカーが特定の条件を満たすまで待機し、別のワーカーがその条件を満たしたら通知する、といった処理が必要になる場合があります。このとき役立つのが Atomics.wait
と Atomics.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!
- この例では、ワーカー
waiter
がAtomics.wait
によってインデックス0
の値が0
の間は待機状態になります。一方 ワーカーnotifier
がAtomics.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チャンネルもご覧ください。