こんにちは!フロントエンドからNode.jsの内部挙動まで、日夜JavaScriptのコードと向き合っているシニアアーキテクトの私です。
JavaScriptの学習を進めていくと、「変数ってどうやって宣言するんだっけ?」「スコープって何だろう?」という基本から始まり、やがて「複数の処理を同時に動かしたい(並行処理)」という壁にぶつかりますよね。
今回は、その並行処理の最前線である「Workerスレッド間での変数共有」をテーマに、JavaScriptの心臓部であるメモリ管理と同期処理の世界を一緒に覗いてみましょう。
「なんだか難しそう……」と感じるかもしれませんが、ここをクリアすれば、あなたのJavaScriptの理解度は間違いなく一段上のステージに到達しますよ。さあ、一緒に紐解いていきましょう!
—
1. なぜ「変数共有」がJavaScriptの難所なのか?
普段、私たちが書いているJavaScriptのコードは、基本的に「メインスレッド」と呼ばれる単一の作業台の上で上から下へと実行されています。
しかし、重い処理(画像処理や数百万件のデータ計算など)を同じ作業台でやらせると、画面がフリーズしてユーザーをイライラさせてしまいますよね。そこで登場するのが、別作業員である「Web Worker(あるいはNode.jsのWorker Threads)」です。
従来の「コピー」の限界
これまで、メインスレッドとWorkerの間でデータをやり取りするには、`postMessage()`という機能を使っていました。これは例えるなら、「手紙のコピーを作って別室の作業員に渡す」という方法です。
[メインスレッド] ──(データのコピーを送信)──> [Workerスレッド]
安全ですが、数ギガバイトもある巨大なデータを扱うとき、毎回コピーを作るのはメモリの無駄遣いだし、何より時間がかかってしまいます。「コピーするんじゃなくて、同じ黒板(メモリ)を共有して同時に見られたら速いのに!」――それを可能にするのが、今回主役となる `SharedArrayBuffer` です。
—
2. SharedArrayBuffer:メモリの直結とスコープの管理
`SharedArrayBuffer`(SAB)は、その名の通り、複数のスレッド間で完全に同一のメモリ領域(バイト列)を共有するための仕組みです。
[メインスレッド] \
═══ [ SharedArrayBuffer (共有メモリ) ]
[Workerスレッド] /
言葉にするとシンプルですが、ここに「スコープの管理」という重要な落とし穴があります。
基本的な使い方とコードのイメージ
まずは、メインスレッドからWorkerへ `SharedArrayBuffer` を渡す基本的なコードを見てみましょう。
// 【メインスレッド側のコード】
// 1. 4バイト(32ビット整数1つ分)の共有メモリ領域を確保する
const sharedBuffer = new SharedArrayBuffer(4);
// 2. データを扱いやすくするために、型付き配列(Int32Array)のビューを被せる
const sharedArray = new Int32Array(sharedBuffer);
// 初期値を入れてみる
sharedArray[0] = 42;
console.log(‘初期値:’, sharedArray[0]); // 42
// 3. Workerスレッドを生成し、SharedArrayBufferを渡す
const worker = new Worker(‘worker.js’);
worker.postMessage(sharedBuffer);
// 1秒後にWorker側で書き換えられた値を確認する
setTimeout(() => {
console.log(‘1秒後の値:’, sharedArray[0]); // 100 に変わっているはず!
}, 1000);
// 【worker.js(Workerスレッド側のコード)側の処理】
self.onmessage = (event) => {
// メインスレッドから送られてきた SharedArrayBuffer を受け取る
const sharedBuffer = event.data;
// 同じメモリ領域に対して、再び Int32Array のビューを作る
const sharedArray = new Int32Array(sharedBuffer);
// メモリを直接書き換える!
sharedArray[0] = 100;
console.log(‘Worker側で値を100に変更しました’);
};
このように、別々のスレッド(スコープ)から、同じメモリ上のデータを直接読み書きできるのが `SharedArrayBuffer` の強力な点です。
—
3. 恐ろしい魔物「データ競合(Race Condition)」の正体
ここで、少し意地悪な想像をしてみましょう。
もし、メインスレッドとWorkerスレッドが、「まったく同じ瞬間に、同じ変数を書き換えようとしたら」どうなるでしょうか?
- メインスレッド:「この値を `+1` しよう(今の値は 10 だから 11 にしよう)」
- Workerスレッド:「この値を `2` しよう(今の値は 10 だから 20 にしよう)」
コンピュータの内部(V8エンジンが解釈する機械語のレベル)では、「値を読み取る」「計算する」「書き込む」という作業は、実は細かなステップに分かれています。
1. 読み取り: スレッドAが「10」を読む
2. 読み取り: スレッドBも「10」を読む
3. 書き込み: スレッドAが「11」と書き込む
4. 書き込み: スレッドBが「20」と書き込む(スレッドAの変更が消えてしまう!)
この現象をデータ競合(Race Condition)と呼びます。バグの特定が非常に難しく、プログラムが気まぐれにクラッシュしたり、おかしな値を叩き出したりする悪夢の原因になります。
—
4. Atomicsでスレッドの交通整理をする
このデータ競合という魔物を防ぐために用意されているのが `Atomics` オブジェクトです。
`Atomics` は、共有メモリに対する操作を「アトミック(不可分=途中で割り込みができない単位)」にするためのメソッドを提供してくれます。交通整理の信号機のようなものだと思ってください。
先ほどの「安全に値を更新する」処理を、`Atomics` を使って書き換えてみましょう。
// Workerスレッド側、あるいはメインスレッド側での安全な加算処理
// sharedArray[0] の値に安全に 5 を足したい場合
// 「現在の値に指定した数値を足して、古い値を返す」というアトミックな操作
const previousValue = Atomics.add(sharedArray, 0, 5);
console.log(‘足す前の値:’, previousValue);
console.log(‘足した後の値:’, sharedArray[0]);
`Atomics.add` を使えば、V8エンジンとOSの低レイヤレベルで排他制御が行われ、他のスレッドが割り込む隙を与えずに安全に計算を完了させることができます。
さらに高度な同期:Atomics.wait と Atomics.notify
「相手の処理が終わるまで、このスレッドを一時停止させたい」という場面もありますよね。そんなときは `Atomics.wait` と `Atomics.notify` を使います。
// 【Worker側:メインスレッドからの合図を待ち受ける】
console.log(‘メインスレッドからの合図を待っています…’);
// sharedArrayのインデックス0番が「0」である間は、ここでスレッドの実行をブロック(一時停止)する
Atomics.wait(sharedArray, 0, 0);
console.log(‘合図を受け取りました!処理を再開します。’);
// 【メインスレッド側:準備ができたら合図を送る】
// データを準備完了の状態にする
sharedArray[0] = 1;
// waitして待っているスレッドを起こす(通知する)
Atomics.notify(sharedArray, 0, 1);
この仕組みを使えば、複雑なマルチスレッドのパイプライン処理や、独自のロック機構をJavaScriptで構築することが可能になります。
—
5. 開発現場でハマりやすい注意点とセキュリティ
最後に、現場で開発する際に絶対に知っておくべき実務的な注意点をお伝えします。
1. セキュリティヘッダー(CORS / COOP / COEP)の壁
`SharedArrayBuffer` は非常に強力な機能であるため、悪意あるコード(サイドチャネル攻撃など)に利用されるリスクがあります。そのため、モダンブラウザで `SharedArrayBuffer` を利用するには、サーバー側で以下のレスポンスヘッダーを正しく設定する必要があります。
- `Cross-Origin-Opener-Policy: same-origin`
- `Cross-Origin-Embedder-Policy: require-corp`
(ローカル開発で動かないときは、大抵これが原因です!)
2. メインスレッドでの `Atomics.wait` の禁止
ブラウザのメインスレッド(画面を描画したりユーザーのクリックを検知したりする大切な場所)で `Atomics.wait()` を呼ぶと、ブラウザが完全にフリーズ(死活状態)するため、JavaScriptの実行環境(V8)によって厳重にエラーとして弾かれます。`wait` を使えるのは、原則としてWorkerスレッド内だけです。
—
まとめ:低レイヤを知ることで、JavaScriptはもっと楽しくなる
今回は、Workerスレッド間での変数共有という少しディープな領域を、`SharedArrayBuffer` と `Atomics` を通して解説しました。
- `SharedArrayBuffer` を使えば、スレッド間でメモリを直接共有し、高速なデータ授受ができる。
- しかし、そのままではデータ競合のリスクがあるため、同時書き換えには注意が必要。
- `Atomics` を活用することで、低レイヤレベルで安全な変数の操作やスレッド間の同期を実現できる。
JavaScriptは「手軽に書けるスク言語」という側面を持ちながら、ブラウザやNode.jsの進化によって、こうしたハードウェアに近い並行処理のプリミティブまでしっかりと扱える言語へと進化しています。
ここをマスターできれば、パフォーマンスに妥協しないリッチなWebアプリケーションや、Node.jsでの高負荷なバックエンド処理も怖くありません。ぜひ、ご自身の環境でも実際にコードを動かして、このスレッド間の協調を体感してみてくださいね。
あなたのJavaScriptの旅が、さらに実りあるものになりますように!