2025年03月25日
こんにちは!今日もコーディングを楽しんでいますか?
こんかいは、JavaScript 初級者を卒業した方に向けて、Web Worker(ウェブワーカー)について解説します。「よく聞くけど、一体何?」「なんだか難しそう…」と思っている方に向けた内容なので、リラックスして読んでみてくださいね。
Web Worker とは?
JavaScript を使ったサイトを作っていると、ブラウザが一瞬フリーズするような場面に遭遇することがあります。例えば、次のようなケースです。「インターバル処理」と書かれた部分は、0.1 秒ごとに 0~999 までのランダムな数字を表示し続けるようにしています。
ここで「重い処理スタート」ボタンをクリックすると、全国市町村郵便番号のデータベースから、「新川」を含む地名を取得し、さらに7桁の郵便番号の中から素数だけを抽出して表示します。
ところがこのとき、ブラウザが少しの間フリーズし、「インターバル処理」の数字の書き換えが止まってしまいます(具体的には、画面に現れる「重い処理にかかった時間」だけ、フリーズしています)。
では、なぜフリーズするのか? 実はこのコードでは、素数の判定に「試し割り法 (Wikipedia)」を使用しています。以下はそれを行なう関数です。
/**
* 「試し割り法」で素数かどうかを検証
*
* @param {number} n 判定する数
* @returns {boolean} 素数なら true
*/
const trialDivision = (n) => {
if (n < 2) {
return false;
}
// 1 ずつ数値を上げて、割り切れるかどうかを検証
for (let i = 2; i < n; i++) {
// 割り切れるか判定
if (n % i === 0) {
// 剰余がなければ割り切れているので、素数ではない
return false;
}
}
// 最後まで割り切れなければ、それは素数である
return true;
};
この関数では、for
ループを使って数値が素数かどうかを判定しています。しかし、JavaScript は原則として、ループ処理中に他の処理を実行できません。そのため、エンドユーザーからはブラウザがフリーズしたように見えてしまったのです。
こうした「回数の多いループなど、他の処理が実行できない状態」によるブラウザのフリーズを防ぐ仕組みとして考案されたのが、この Web Worker です。
スレッドとは?
プログラミングの世界では、処理の流れを「スレッド(Thread)」と呼びます。スレッドとは、「独立して動作する処理の単位」のことです。
JavaScript はもともと「シングルスレッド」の言語でした。つまり文字通り、1つのスレッド(処理の流れ)しか持たず、ひとつの処理が終わるまでは次の処理を実行できませんでした。最初の例で、ループ処理でブラウザがフリーズしてしまっていたのは、このためです。
現在 JavaScript は「マルチスレッド」の言語として再定義されています。つまり、「メインスレッド」とは別に「サブスレッド(Worker)」を生成することで、重い処理やループをバックグラウンドで実行させて、ブラウザのフリーズを防ぐことができるようになりました。Web Worker は、この「サブスレッド」を生成する仕組みです。
スレッド同士は独立した世界
スレッドが複数動作している最中に、互いに干渉し合うと問題が起こることがあります。そのため、スレッド同士は基本的に「独立した世界」で動作します。この性質を「スレッドセーフ」と呼びます。
しかし、完全に独立してしまうと、データの受け渡しができません。そこで、スレッド間で「メッセージ」を送受信することでデータをやり取りする仕組みが用意されています。
Web Worker の実例
では、最初の例でブラウザをフリーズさせていた「素数を得るための処理」を、「サブスレッド」で行わせるようにした例をご覧ください。
「重い処理スタート」ボタンをクリックしてからの結果は同じですが、「インターバル処理」の動作は止まりません。つまり、ブラウザがフリーズしていないことが確認できることと思います。また、「重い処理にかかった時間」の数字はあまり変化していないことにもご注目ください(つまり、素数を求める処理は、バックグラウンドで間違いなく行なわれているのです)。
では、このコードを解説していきましょう。
Web Worker を生成する
まず、 script.js の冒頭の記述に着目してください。
/** Worker */
const worker = new Worker('./lib/worker.js');
worker
つまりサブスレッドは、 new Worker('HTML ファイルから見たJSファイルのパス')
で生成します。この JSファイル
には、素数を求める処理が書かれています。
次に、そのファイル( worker.js )の内容を見てみましょう。
/**
* メッセージ受信時の処理
*
* @param {MessageEvent} e メッセージイベント
* @param {string[][]} e.data イベントに添えられたメッセージ
*/
self.onmessage = (e) => {
const locations = e.data;
self.postMessage(filterPrime(locations));
};
スレッド同士は独立した世界と説明しました。サブスレッドにとっての「自分の世界」は、 self
オブジェクトで表現します(この例では、ファイルの内容がサブスレッドであることは自明なので、 self
は省略可能です)。
self.onmessage
には、メインスレッドからメッセージを受け取ったときに実行するべき処理を、関数オブジェクトとして記述します。- この関数オブジェクトが受け取る引数は
MessageEvent
オブジェクトというものです。その.data
プロパティには、「メインスレッドからのメッセージ」が入っています。 self.postMessage(メッセージ)
メソッドを実行することで、このサブスレッドからメインスレッドに、メッセージを送信します。通常、このメッセージ
とは、サブスレッドでの処理結果となることでしょう。
この例では、メインスレッドからのメッセージとして「CSVファイルの各行を列ごとに分解した、文字列の二重配列」がわたってくることを想定しています。コードを見ていただければわかるように、それ以外の部分では、最初のコードでの「素数を判定する処理」と変わるところはありません。
Web Worker とメッセージを送受信する
ふたたび、 script.js を見てゆきましょう(コードは簡易化しています)。
// サブスレッドからのメッセージを待ち受けるイベントを定義
worker.addEventListener('message', onMessage);
// ボタンがクリックされたときのイベントを定義
btnBegin.addEventListener('click', main);
/**
* メイン処理
*
* @returns {Promise<string>}
*/
async function main() {
/** 素数を抜き出す処理 */
worker.postMessage(locations);
}
/**
* サブスレッドからのメッセージ受信時の処理
*
* @param {MessageEvent} e メッセージイベント
* @param {string[][]} e.data イベントに添えられたメッセージ
*/
async function onMessage(e) {
const result = e.data;
showResult(head.split(','), result);
};
この「メインスレッド」での処理は、以下のとおりです。
worker.addEventListener('message', onMessage)
で、サブスレッドからのメッセージを待ち受けるイベントを定義します。- ボタンが押されたときのイベントの中で、
worker.postMessage(メッセージ)
で、そのサブスレッドにメッセージ
として「CSVファイルの各行を列ごとに分解した、文字列の二重配列」を送信します。 onMessage(e)
関数が受け取る引数は、サブスレッド同様MessageEvent
オブジェクトで、.data
プロパティには「サブスレッドからのメッセージ」が入っています。
あとは最初の例と同様に、このメッセージを処理すればOKです。お疲れ様でした!
メッセージは「参照」を持たない!
JavaScript にある程度慣れた方なら、オブジェクトの「参照」についてご存知だと思います。しかし、スレッド同士は独立した世界であることを担保するため、スレッド間のメッセージが参照で紐づく事はありません。メッセージはそのデータ種別によって、複製 (Copy) または 移譲 (Transfer) の形で送られます。
委譲 (Delegation) と 移譲 (Transfer) は、どちらも「いじょう」と読みます。混同しないよう気をつけてください!
それぞれに対応するメッセージは、次の通りです。
- 複製:構造化複製アルゴリズムに対応した値、つまりほとんどの種類のメッセージが該当します。サブスレッドへ送信するメッセージは、メインスレッドで指定したメッセージのコピーです。
- 移譲:
ArrayBuffer
など、いくつかの種類のオブジェクトのメッセージが該当します。サブスレッドにそのメッセージを送信すると、メインスレッドからそのデータは消滅してしまいます。サブスレッドからのメッセージがその処理結果であれば、あくまでも別の値として受け取ることは可能です。
メインスレッドとサブスレッドとの違い
ブラウザにおけるメインスレッドでは、ルートオブジェクトは window
です。一方で Web Worker によって生成されたサブスレッドでは、ルートオブジェクトは self
です。
これによって、サブスレッドではできないことや、サブスレッドでなければできないことがあります。
サブスレッドではできないこと(一例)
- DOM を直接参照、操作すること (
document.createElement()
など) XMLHttpRequest
のresponseXML
とchannel
プロパティへのアクセス (常にnull
になる)
とくに DOM にまつわる操作は許されないことは、慣れないうちは忘れがちですので、気をつけてくださいね。
サブスレッドなければできないこと(一例)
importScript('JSファイルへのパス')
による、他のJSファイルの実行時読み込みFileReaderSync()
による、任意のファイルの同期的な読み込み
上記以外のたいていの事は、メインスレッドとサブスレッドの間で、できることに差はありません。とくに、ゲームアプリでキャラクターのアニメパターンの画像ファイルを読み込んでビットマップデータを生成したり、DTMアプリでメモリからオーディオデータを生成したりといった、高度で複雑な計算の繰り返しは、サブスレッドで行わせるのが適しています。
まとめ
Web Worker によるマルチスレッド・プログラミングは革命的な機能ですが、一方で、ほんらい交わるはずのない世界同士での相互作用を、プログラマ自身が管理しなければなりません。スレッドセーフを守るためのルールも厳格で、慣れるまでは戸惑うことも多いでしょう。しかし、いちど慣れてしまうと、スレッドセーフを守るための「潔癖なコード」を書く習慣が自然と身についてきます。お仕事で使うことはないという方も、いちど日曜大工として Web Worker で遊んでみてはいかがでしょうか? うまく動作したときは、とても気持ちがいいですよ!
こんかいのお話は、以上です。それでは、よきコーディングライフを!