こんにちは!フロントエンドからNode.jsの深部、そしてV8エンジンの挙動まで、日夜JavaScriptと向き合っているシニアアーキテクトです。
今回は、JavaScriptの変数スコープと並列処理の最前線である「Workerスレッド間での変数共有:SharedArrayBufferとAtomics」についてお話しします。
「JavaScriptはシングルスレッドだから安全」というのは、もう昔の話です。現代のWebアプリケーションやNode.jsでは、重い処理をバックグラウンドに逃がすために「Web Worker(あるいはWorker threads)」を当たり前のように使いますよね。
でも、スレッドが増えると「あれ?この変数の値、今どっちのスレッドから見えているんだっけ?」というスコープの境界線で混乱したことはありませんか?今回は、そのモヤモヤを完全に解消し、マルチスレッド環境でも安全に変数を共有するための極意を優しく紐解いていきますよ。ここをクリアすれば、あなたのJavaScriptのスキルは間違いなくワンランク上のステージに到達します!
—
1. なぜ「変数共有」でJavaScriptの常識が変わるのか?
私たちが普段使っている `let` や `const`、そしてオブジェクトのプロパティは、基本的に「ひとつのスレッド(メインスレッド)」の中だけで完結しています。
従来のWorker間通信(`postMessage`)を思い出してください。データを送るときは、メインスレッド側でオブジェクトを複製(コピー)してWorkerに投げ渡していましたよね。
[メインスレッド] (変数A: 100) –(コピーを送信)—> [Workerスレッド] (変数A: 100のコピー)
※別々のメモリ領域なので、片方を書き換えてももう片方は変わらない
これは安全ですが、巨大な画像データや大量の数値データをやり取りするたびにメモリのコピーが発生し、パフォーマンスがガタ落ちする原因になります。
そこで登場するのが、`SharedArrayBuffer` です。これは、文字通り「複数のスレッド間で同じメモリ領域を直接共有する」ための仕組みです。
[メインスレッド] \
=== 共有メモリ (SharedArrayBuffer) ===
[Workerスレッド] /
※同じメモリを直接読み書きする!
「おっ、じゃあ `let` や `const` のように、一つの変数を両方のスレッドから書き換えられるんだな!」と思いましたか?
……実は、話はそんなに単純ではないんです。ここにマルチスレッドプログラミング特有の大きな罠が潜んでいます。
—
2. スコープの壁を超えたメモリ共有の正体
まず大前提として、`let` や `const` といった変数宣言そのものが、スレッドを越えて共有されるわけではありません。
JavaScriptのスコープは、あくまで「そのスレッド(実行コンテキスト)」の中だけのものです。メインスレッドにある `let x = 10` という変数は、Workerスレッドから直接名前で参照することはできません。
共有できるのは、変数そのものではなく、「メモリーバッファ(生データの置き場所)」です。
具体的にどう書くのか、コードを見てみましょう。
メインスレッド側のコード例
// 1. 4バイト(32ビット整数1つ分)の共有メモリ領域を作成する
const sharedBuffer = new SharedArrayBuffer(4);
// 2. このメモリを「32ビット符号付き整数」として扱いたいのでビューを作る
const sharedArray = new Int32Array(sharedBuffer);
// 初期値を入れてみる
sharedArray[0] = 42;
console.log(‘メインスレッド: 初期値設定完了’, sharedArray[0]);
// 3. Workerスレッドを立ち上げて、このバッファを渡す
const worker = new Worker(‘worker.js’);
worker.postMessage(sharedBuffer);
Workerスレッド側のコード (`worker.js`)
// メインスレッドから共有バッファを受け取る
self.onmessage = (event) => {
const sharedBuffer = event.data;
// 同じバッファからInt32Arrayのビューを作る
const sharedArray = new Int32Array(sharedBuffer);
// メインスレッドで作ったメモリの値を読み取れる!
console.log(‘Workerスレッド: 受け取った値は’, sharedArray[0]); // 42
// 値を書き換えてみる
sharedArray[0] = 100;
console.log(‘Workerスレッド: 値を書き換えました’);
};
このように、`SharedArrayBuffer` を使えば、メインスレッドとWorkerスレッドが同じメモリの蜂の巣(アドレス)を共有し、お互いに読み書きができるようになります。
—
3. ここが怖い!並列処理で陥る「見えないバグ」
「おぉ、すごい!これなら高速にデータを共有できるじゃないか!」と思いましたよね。
しかし、ここでプログラミング初学者が必ずと言っていいほどハマる、並列処理の悪夢についてお話しなければなりません。
もし、メインスレッドとWorkerスレッドが「同時に」同じメモリの場所を書き換えようとしたら、どうなるでしょうか?
- メインスレッド:「ここに 1 を足そう!」
- Workerスレッド:「ここに 2 を掛けよう!」
CPUの内部では、メモリの「読み込み (Read)」「計算 (Compute)」「書き込み (Write)」という3つのステップがバラバラのタイミングで実行されます。そのため、両者が猛スピードで同じメモリをいじくり回すと、「片方の変更が消えてしまう」「変なゴミデータが読み込まれる(レースコンディション)」という、再現性の極めて低い恐ろしいバグが発生します。
さらに、V8エンジンや現代のCPUは、パフォーマンスを最適化するために、コードの実行順序をこっそり入れ替えたり(命令の並び替え)、CPUキャッシュにデータを溜め込んだりします。そのため、「こっちのスレッドで書き換えたのに、あっちのスレッドから見えていない!」という現象が平気で起きるのです。
—
4. 救世主 `Atomics`:スレッド間の調停者
「じゃあ、マルチスレッドなんて怖くて使えないよ……」と思いましたか?ご安心ください。JavaScriptには、この混沌を制御するための強力なガードマンが用意されています。それが `Atomics` オブジェクトです。
`Atomics` を使うと、メモリの読み書きを「アトミック(不可分=途中で割り込みができない単位)」に保証し、さらにスレッド間の「同期(待ち合わせ)」を行うことができます。
先ほどのコードを、`Atomics` を使って安全に書き換えてみましょう。
安全な値の更新(アトミックな加算)
// ただの「sharedArray[0] += 1」ではなく、Atomicsを使う!
// これにより、「読み込んで、足して、書き込む」一連の処理が絶対に途中で中断されない
Atomics.add(sharedArray, 0, 1);
安全な値の読み取り
// CPUキャッシュを強制的にバイパスし、確実に最新のメモリの値を安全に読み取る
const currentValue = Atomics.load(sharedArray, 0);
console.log(`現在の正確な値: ${currentValue}`);
スレッドを眠らせて待つ(Atomics.wait と Atomics.notify)
これが `Atomics` の真骨頂です。例えば、「Worker側で特定の計算が終わるまで、メインスレッドをピタッと止めて待たせたい」という場合に使います。
- Worker側(計算完了を通知):
sharedArray[0] = 999; // データを更新
Atomics.store(sharedArray, 0, 999);
Atomics.notify(sharedArray, 0, 1); // 待っているスレッドを1つ起こす
- メインスレッド側(通知が来るまでスレッドをブロックして待機):
console.log(‘Workerの完了を待っています…’);
// 値が999になるまで、CPUリソースを無駄に消費せず安全に待機する
Atomics.wait(sharedArray, 0, 42); // 値が42から変わるまで待つ
console.log(‘Workerの処理が完了しました!’);
(※注意: `Atomics.wait` はメインスレッドのUIをフリーズさせてしまうため、実際のブラウザ環境のメインスレッドでは実行できません。Workerスレッド内、またはNode.jsのワーカースレッド等で利用します)
—
まとめ:変数のスコープから「メモリの空間」へ視座を上げよう
いかがでしたでしょうか?
- 変数のスコープ(`let` / `const`)は、あくまで1つのスレッド内での名前解決のルール。
- `SharedArrayBuffer` は、スレッドの壁を越えて「物理的なメモリ領域」を共有する仕組み。
- `Atomics` は、共有されたメモリを安全に料理するための厳格なルールと調停役。
JavaScriptをただの「ブラウザで動くお手軽な言語」として見ているうちは、これらの概念に出会うことはありません。しかし、V8エンジンのメモリ構造やOSのマルチスレッドの仕組みまで意識できるようになると、JavaScriptで見える世界がガラリと変わります。
最初は少し難しく感じるかもしれませんが、「スレッド間でメモリを直接触る時は、必ず `Atomics` というガードマンを挟むんだな」と覚えておけば大丈夫です。
この壁を越えたあなたなら、どんなに重い処理を伴うフロントエンドアプリや、高パフォーマンスなNode.jsバックエンドでも自信を持って設計できるようになりますよ。一緒にモダンで堅牢なJavaScript極めていきましょう!