BeAct Co., Ltd.

BLOG
社員ブログ

Web Worker を使ってみよう!

こんにちは!今日もコーディングを楽しんでいますか?

こんかいは、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() など)
  • XMLHttpRequestresponseXML と channel プロパティへのアクセス (常に null になる)

とくに DOM にまつわる操作は許されないことは、慣れないうちは忘れがちですので、気をつけてくださいね。

サブスレッドなければできないこと(一例)

上記以外のたいていの事は、メインスレッドとサブスレッドの間で、できることに差はありません。とくに、ゲームアプリでキャラクターのアニメパターンの画像ファイルを読み込んでビットマップデータを生成したり、DTMアプリでメモリからオーディオデータを生成したりといった、高度で複雑な計算の繰り返しは、サブスレッドで行わせるのが適しています。

まとめ

Web Worker によるマルチスレッド・プログラミングは革命的な機能ですが、一方で、ほんらい交わるはずのない世界同士での相互作用を、プログラマ自身が管理しなければなりません。スレッドセーフを守るためのルールも厳格で、慣れるまでは戸惑うことも多いでしょう。しかし、いちど慣れてしまうと、スレッドセーフを守るための「潔癖なコード」を書く習慣が自然と身についてきます。お仕事で使うことはないという方も、いちど日曜大工として Web Worker で遊んでみてはいかがでしょうか? うまく動作したときは、とても気持ちがいいですよ!

こんかいのお話は、以上です。それでは、よきコーディングライフを!