【入門編】Workerスレッド間での変数共有:SharedArrayBufferとAtomicsによる並列処理のスコープ管理 – JavaScriptコア文法・モダン言語仕様と変数・関数解析バイブル

みなさん、こんにちは!日々JavaScriptを書いていて、「変数」や「スコープ」の仕組みには少しずつ慣れてきたでしょうか?

通常のJavaScript開発では、`const` や `let` を使って「関数の内側」や「ブロックの内側」といったスコープの中で変数を管理しますよね。JavaScriptは基本的にシングルスレッド(1つの作業員が順番に処理を行う仕組み)で動いているため、変数の書き換えによる競合はほとんど意識せずに済みます。

しかし、重い計算処理をバックグラウンドで行うためにWeb Worker(マルチスレッド)を使い始めると、話が一変します。

スレッド同士で「同じ変数(データ)」を一緒に使いたいとき、ただの変数ではスコープを超えて共有することができません。そこで登場するのが`SharedArrayBuffer`と`Atomics`です。

今回は、V8エンジンのメモリ空間の裏側で何が起きているのかをイメージしながら、スレッド間で安全にデータを共有・同期するプログラミングの極意を、わかりやすく解説していきますね。ここをクリアすれば、JavaScriptの並列処理の基本はバッチリマスターできますよ!

—

1. なぜ通常の変数はWorker間で共有できないのか?

まず、JavaScriptエンジン(ChromeのV8など)がどのようにコードを実行しているかをイメージしてみましょう。

JavaScriptの実行環境には「Isolate(アイソレート)」と呼ばれる独立した部屋があります。メインスレッドとWeb Workerは、それぞれ完全に独立した部屋(Isolate)と独自のメモリ領域(ヒープ)を持っています。

【メインスレッド(部屋A)】 【Web Worker(部屋B)】
+———————–+ +———————–+
| V8 Heap (メモリ空間) | | V8 Heap (メモリ空間) |
| let count = 0; | | |
+———————–+ +———————–+
│ ▲
└────────── (データのコピー) ──────────┘
postMessage()

そのため、メインスレッドで宣言した `let count = 0;` という変数は、Workerからは逆立ちしても直接見ることができません。

従来の `postMessage` の限界

Workerにデータを渡す際、通常は `postMessage()` を使いますよね。しかし、これは内部的に「データの深いコピー(structuredClone)」を行っています。つまり、100MBの巨大なデータをWorkerに送るたびに、100MB分のメモリコピーが発生し、パフォーマンスが低下してしまうのです。

「コピーするのではなく、同じメモリ領域を両方から直接読み書きしたい!」
その願いを叶えるのが、これから紹介する `SharedArrayBuffer` です。

—

2. 共有メモリの罠:「レースコンディション(データ競合)」

`SharedArrayBuffer` は、複数のスレッドが同時にアクセスできる「共有された生のメモリ領域(バイト列)」領域をメモリ上に確保します。

【メインスレッド】 【Web Worker】
│ │
└───► [ SharedArrayBuffer (共有メモリ) ] ◄───┘

「これで自由に変数を書き換えられる!」と思うかもしれません。しかし、ここに恐ろしい罠が潜んでいます。それがレースコンディション(競合状態)です。

例えば、共有メモリ上にある数値 `10` に対して、メインスレッドとWorkerが同時に「1を足す」処理を行おうとした場面をイメージしてください。

悲劇のシナリオ(CPUレベルで起きていること)

1. メインスレッドが「現在の値(10)」を読み込む。
2. Workerもまったく同時に「現在の値(10)」を読み込む。
3. メインスレッドが 10 + 1 = `11` をメモリに書き込む。
4. Workerも 10 + 1 = `11` をメモリに書き込む。

本来なら 10 + 1 + 1 で `12` になってほしいはずなのに、結果は `11` になってしまいました!
このように、複数のスレッドが順序の保証なく同時にメモリを操作すると、データが狂ってしまうのです。

—

3. 救世主 `Atomics`:絶対に割り込ませない「原子操作」

このカオスを解決するのが、JavaScriptの組込みオブジェクト `Atomics`(アトミクス)です。

英語の「Atomic(原子の / これ以上分割できない)」という言葉が示す通り、`Atomics` が提供するメソッドは「読み込み・計算・書き込み」の一連の処理を、他のスレッドに一切割り込ませずに一気に行う(アトミック操作)ことを保証してくれます。

銀行のATMで「残高確認と引き落とし」を同時にロックして行うようなイメージですね。

主な `Atomics` メソッド

  • `Atomics.add(typedArray, index, value)`: 指定位置の値に加算(一気に実行!)
  • `Atomics.load(typedArray, index)`: 安全に値を読み込む
  • `Atomics.store(typedArray, index, value)`: 安全に値を書き込む
  • `Atomics.wait(typedArray, index, value)`: 条件を満たすまでスレッドを一時停止(スリープ)させる
  • `Atomics.notify(typedArray, index, count)`: 停止中のスレッドを起こす

—

4. 実践!SharedArrayBufferとAtomicsを使った並列カウンタ

それでは、実際に動くコードを見てみましょう!
メインスレッドとWorkerが、1つの `SharedArrayBuffer` を共有し、安全にカウントアップするプログラムを作成します。

① メインスレッド側のコード (`main.js`)

// 1. 4バイト(Int32 1つ分)の共有メモリを確保
const sharedBuffer = new SharedArrayBuffer(4);

// 2. 共有メモリを32ビット符号付き整数配列として扱うためのビュー(操作用窓口)を作成
const sharedArray = new Int32Array(sharedBuffer);

// 初期値を 0 に設定(インデックス 0 を使用)
Atomics.store(sharedArray, 0, 0);

console.log(`[メイン] 初期値: ${Atomics.load(sharedArray, 0)}`);

// 3. Workerを作成
const worker = new Worker(‘worker.js’);

// Workerに SharedArrayBuffer を送信(※コピーではなく参照が共有されます!)
worker.postMessage(sharedBuffer);

// Workerからの完了報告を受け取る
worker.onmessage = () => {
// メインスレッド側でも値を1回増やす
Atomics.add(sharedArray, 0, 1);

// 最終結果の確認(0 + 1000[Worker分] + 1[メイン分] = 1001 になるはず!)
console.log(`[メイン] 最終カウント結果: ${Atomics.load(sharedArray, 0)}`);
};

② Worker側のコード (`worker.js`)

// メインスレッドから SharedArrayBuffer を受け取る
self.onmessage = (event) => {
const sharedBuffer = event.data;
const sharedArray = new Int32Array(sharedBuffer);

console.log(‘[Worker] カウントアップを開始します…’);

// 1000回安全にカウントアップを行う
for (let i = 0; i < 1000; i++) { // Atomics.add を使うことで、他のスレッドの割り込みを許さずに+1する Atomics.add(sharedArray, 0, 1); } console.log('[Worker] カウントアップ完了!'); // メインスレッドに完了を通知 self.postMessage('DONE'); };

実行結果(コンソール出力)

[メイン] 初期値: 0
[Worker] カウントアップを開始します…
[Worker] カウントアップ完了!
[メイン] 最終カウント結果: 1001

`Atomics.add` を使うおかげで、一回転もデータ競合を起こすことなく、正確に `1001` という結果を得ることができました!

—

5. 初学者がハマりやすい2つの罠(注意点)

`SharedArrayBuffer` を実践で使う際、誰もが一度は遭遇する「陥りやすいエラー」と「ルール」を紹介しておきますね。

罠①:メインスレッドで `Atomics.wait()` を呼ぶとクラッシュする

`Atomics.wait()` は「指定した値が変わるまでスレッドを停止して待つ」という強力な同期機能です。
しかし、ブラウザのメインスレッドで `Atomics.wait()` を呼ぶと `TypeError` が発生します。

// ❌ メインスレッドでやってはダメ!
Atomics.wait(sharedArray, 0, 0); // Uncaught TypeError: Atomics.wait cannot be called in this context

なぜなら、メインスレッドを停止させると画面の描画やユーザーの操作(クリックなど)まで完全にフリーズしてしまうからです。`Atomics.wait()` は、画面描画を担わない Workerスレッド内でのみ使用する というルールを覚えておきましょう。

罠②:セキュリティヘッダー(COOP / COEP)の設定が必要

ブラウザで `SharedArrayBuffer` を使おうとすると、`SharedArrayBuffer is not defined` というエラーが出ることがあります。

これは、過去に発見されたCPUの脆弱性(Spectreなど)からユーザーを守るためのセキュリティ制限です。`SharedArrayBuffer` を有効化するには、Webサーバー側で以下の2つのHTTPレスポンスヘッダーを出力する必要があります。

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

ローカル開発で Vite や webpack を使っている場合は、開発サーバーの設定でこれらのヘッダーを付与するように設定してくださいね。

—

まとめ:並列処理をマスターして次のステージへ!

今回は、普段意識する「スコープ」のさらに外側、スレッド境界を超えたメモリ共有の世界を覗いてみました。ポイントを整理しておきましょう!

1. 通常変数や `postMessage` はデータのコピーを行うため安全だが、巨大データの扱いには遅い。
2. `SharedArrayBuffer` を使うと、メインスレッドとWorkerで同じメモリ空間を直接共有できる。
3. データの競合を防ぐために、読み書きには必ず `Atomics` オブジェクトのメソッドを使う。
4. `Atomics.wait()` はWorkerスレッド専用!メインスレッドを止めないように注意する。

最初は少し難しく感じるかもしれませんが、「CPUレベルでデータを安全にやり取りする仕組み」がイメージできるようになると、JavaScriptの非同期処理やV8エンジンの理解が一段と深まりますよ。

ここをクリアできれば、マルチスレッドを駆使した超高速なWebアプリケーション開発の基本はバッチリマスターです!ぜひご自身のコードでも試してみてくださいね。応援しています!

タイトルとURLをコピーしました