【入門編】Web Workerとスコープ:メインスレッドと隔離されたメモリ空間の変数共有 – JavaScriptコア文法・モダン言語仕様と変数・関数解析バイブル

こんにちは!フロントエンドからNode.jsの内部構造まで、JavaScriptの駆動する世界を日々しゃぶり尽くしているチーフアーキテクトです。

今回は、JavaScriptの「変数とスコープ」という一見基礎的なテーマから一歩踏み込み、「Web Workerとスコープ:メインスレッドと隔離されたメモリ空間の変数共有」についてお話しします。

「他の言語ではスレッド間でメモリを共有できるのに、なぜJavaScriptはそれができないんだろう?」
「Workerにデータを渡すとき、何が裏で行われているの?」

ここをクリアすれば、ブラウザのシングルスレッドモデルの本質と、モダンな非同期・並行処理の仕組みが綺麗につながって、JavaScriptの基本はバッチリマスターできますよ。
さあ、V8エンジンのメモリ空間の裏側を覗きにいきましょう!

—

1. なぜJavaScriptのWorkerでは変数を共有できないのか?

まず、JavaScriptの大前提を思い出してください。ブラウザのメインスレッド(JavaScriptが普段動いている場所)は、基本的に「シングルスレッド」で動いています。UIの描画やイベント処理、ユーザーの操作をたった一つの担当者が休むことなくこなしている状態です。

ここに重い処理(例えば、巨大な配列のソートや複雑な暗号化計算など)をそのまま流し込むと、担当者がその作業に手一杯になってしまい、画面がカクついたり、ボタンを押しても反応しなくなったりする「フリーズ現象」が起きます。

そこで登場するのが Web Worker です。
Web Workerを使うと、メインスレッドとは全く別の「バックグラウンドの別作業員(別スレッド)」を雇って、重い処理を丸投げすることができます。

メモリ空間の「完全な壁」

ここで重要なのが、Web Workerの世界とメインスレッドの世界は、メモリ空間が完全に隔離されているという点です。

C言語やJavaなどのマルチスレッドプログラミング経験がある方だと、「同じ変数を別のスレッドから書き換える」ということをやりたくなりますよね。しかし、JavaScriptではそれが絶対にできません。

[メインスレッド] [Web Workerスレッド]
変数 let count = 10; × (共有不可) 変数 count は存在しない!
(V8ヒープ領域 A) (V8ヒープ領域 B)

なぜ共有できないかと言うと、もし同じメモリ領域を複数のスレッドから同時に読み書きできるようにしてしまうと、「Aスレッドが書き換えている最中に、Bスレッドがそれを読み込んでバグる(競合状態 / Race Condition)」という悪名高い問題が起きてしまいます。
JavaScriptは、このメモリの安全性を守るために、「スレッド間でメモリを共有させない」という強固な設計(SharedArrayBufferなど例外を除き)をとっているのです。

—

2. 変数が共有できないなら、どうやってデータをやり取りするのか?

「じゃあ、Workerで計算した結果をどうやってメインスレッドに戻すの?」
「変数が共有できないなら、データの受け渡しはどうやるの?」

ここで使われるのが、「メッセージング(Message Passing)」という仕組みです。
スレッド間で直接変数をいじることはできませんが、「データのコピー(荷物)」を配達員に頼んで相手に送り届けることはできます。

百聞は一見に如かず。実際にコードを見てみましょう。

実装例:メインスレッドとWorkerの連携

まずは、バックグラウンドで動くWorker側のスクリプト(`worker.js`)です。

// worker.js (Web Worker側)

// メインスレッドからメッセージ(荷物)が届いたときに発火するイベント
self.onmessage = function(event) {
// event.data にメインスレッドから送られてきたデータが入っています
const incomingData = event.data;

console.log(‘Worker側でデータを受け取りました:’, incomingData);

// 重い計算のシミュレーション(ここでは受け取った数値を2倍にする)
const result = incomingData.number 2;

// 計算結果をメインスレッドに送り返す
self.postMessage({
message: ‘計算が終わりました!’,
result: result
});
};

次に、メインスレッド側のコードです。

// main.js (メインスレッド側)

// 1. 新しいWeb Worker(別作業員)を生成する
const myWorker = new Worker(‘worker.js’);

// 2. Workerへ送るデータ(変数)を準備する
const payload = {
id: 1,
number: 42,
text: ‘こんにちは、Workerさん!’
};

console.log(‘メインスレッドからデータを送信します:’, payload);

// 3. postMessageを使ってWorkerにデータを送る
myWorker.postMessage(payload);

// 4. Workerから返ってきたメッセージを受け取る
myWorker.onmessage = function(event) {
console.log(‘メインスレッドがWorkerから結果を受け取りました!’);
console.log(event.data.message); // “計算が終わりました!”
console.log(event.data.result); // 84
};

このコードでは、`payload` というオブジェクトを `postMessage` でWorkerに渡しています。このとき、メモリ上の変数を共有しているのではなく、データがコピーされて向こう側に渡っているという点がポイントです。

—

3. 裏側で何が起きているのか?「構造化複製アルゴリズム」の正体

では、`postMessage` でオブジェクトや配列を渡すとき、JavaScriptのランタイム(V8エンジンなど)の内部では一体何が行われているのでしょうか?

ここで登場するのが、構造化複製アルゴリズム(Structured Clone Algorithm)です。

初学者のうちは、「JSON.stringifyしてparseしてるのと同じ?」と思われがちですが、実はもっとスマートで強力です。

構造化複製の特徴

1. 複雑なデータ構造をクローンできる

  • 通常の `JSON.stringify` では消えてしまう `Date` オブジェクト、`RegExp`(正規表現)、`Map`、`Set`、さらにはバイデータ(`ArrayBuffer` など)もそのままコピーして渡すことができます。

2. 循環参照(自分自身をプロパティに持つようなオブジェクト)を扱える

  • JSONだとエラーになるような複雑に絡み合ったオブジェクト構造も、構造化複製であれば正しくコピーしてくれます。

3. 関数(Function)はコピーできない(※重要)

  • JavaScriptの関数はクロージャのスコープチェーンや実行コンテキストを抱えているため、構造化複製では関数をコピーして別スレッドに送ることはできません。もし関数を送ろうとすると、エラー(`DataCloneError`)が発生します。

⚠️ 陥りやすい文法エラー・設計ミス

ここで、よくある開発現場での落とし穴をご紹介します。

const worker = new Worker(‘worker.js’);

// 【やってはいけない例】
const data = {
name: ‘田中’,
// プロパティに関数を入れてしまっている!
sayHello: function() {
console.log(‘こんにちは!’);
}
};

// これはエラーになります!
worker.postMessage(data);
// ➡️ Uncaught DOMException: Failed to execute ‘postMessage’ on ‘Worker’:
// function() { … } could not be cloned.

【解決策】
Workerとやり取りできるのは、あくまで「純粋なデータ(状態)」のみです。関数やDOM要素などは送れません。Worker側で実行させたいロジックがある場合は、関数そのものを送るのではなく、データの種類を判別する「コマンド(アクション名)」と「パラメータ」の組み合わせをオブジェクトとして送るように設計しましょう。

// 良い例:データと「命令」だけを送る
worker.postMessage({
action: ‘CALCULATE_DISCOUNT’,
params: { price: 1000, rate: 0.2 }
});

—

4. パフォーマンスの極み:データを「コピーせず」に渡す(Transferable Objects)

最後に、チーフアーキテクトとしてもう一段階深い知見をお伝えします。

「構造化複製は便利だけど、数メガバイト、あるいは数ギガバイトもある巨大なバイナリデータ(画像データや音声データなど)を毎回コピーしていたら、メモリの割り当てとコピーに時間がかかって、せっかくWorkerを使った意味が薄れてしまうのでは?」

その通りです。そこで用意されているのが Transferable Objects(転送可能オブジェクト) です。

`ArrayBuffer` などのバッファデータは、コピーする代わりに「所有権そのものを相手のスレッドに移動(Transfer)」させることができます。

// メインスレッド側
const buffer = new ArrayBuffer(1024 1024 10); // 10MBのバッファ

// 第2引数に転送したいオブジェクトの配列を指定する
// これにより、コピーコスト「ゼロ」でデータが瞬時にWorkerに移動する
myWorker.postMessage(buffer, [buffer]);

// 注意:所有権が移動するため、この瞬間からメインスレッド側では buffer は空(byteLength === 0)になります!
console.log(buffer.byteLength); // 0

データを「コピー」するのではなく「キャッチボールのグローブごと相手に投げ渡す」イメージです。この仕組みを使いこなせるようになると、ブラウザの画像処理や音声解析といった高負荷なフロントエンドアプリケーションでも、メインスレッドを一切詰まらせることのない滑らかなUIを実現できます。

—

まとめ

今回は、Web Workerとスコープ、そしてメモリ空間の隔離とデータ共有のメカニズムを解説しました。

  • JavaScriptのスレッド間では、メモリ(変数)の直接共有はできない。
  • メインスレッドとWorkerの通信は、変数をそのまま共有するのではなく、構造化複製(Structured Clone)による「データのコピー」で行われる。
  • 関数などは複製できないため、送るのは「純粋なデータオブジェクト」に限定する。
  • 巨大なデータは、コピーコストを回避するために Transferable Objects で所有権を移動させる。

ここをしっかりと理解しておけば、モダンブラウザの非同期並行処理のアーキテクチャ設計で迷うことはもうありません。ぜひ実際のプロジェクトでも、メインスレッドを労わる優しい設計を意識してみてくださいね。それでは、また次の極限の知見でお会いしましょう!

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