はじめに

前回、Web Worker の基本的な事項について、説明してきました。

今回から、Web Worker の使い方について、数回にわたり説明していきたいと思います。

まずは、Web Worker が、起動後、どのようなサイクルで動作しているのかを簡単に説明します。


Web Worker のライフサイクル

一般的なバックグラウンドスレッドの Web Worker は、大きく分けて、

         生成
          │
          ▼
        初期化
  ┌──────▶│
  │      待機
  │       │
  │       ▼ 
  │ メッセージ受信
  │       │
  │       ▼
  │      処理
  │       │
  │       ▼ 
  │ メッセージ送信
  │       │
  └───────┥
          │
          ▼
         終了

というイベントループの流れで動作します。

初期化後、メインスレッドからのメッセージによる依頼を受けて、処理を行い、処理完了後、結果をメインスレッドに返すという繰り返し動作をイベントループで行います。

このイベントループは、メインスレッドのイベントループから完全に独立しており、平行して動作します。


Web Worker の起動/停止

Web Worker は、メインスレッドから Web API を通じて、起動/停止が可能です。

■ Web Worker の起動

Web Worker の起動は簡単です。メインスレッドにて、Web API を使い、Web Worker オブジェクトを生成するだけです。

例えば

  • main.js

メインスレッド用の JavaScript

  • worker.js

Web Worker 用の JavaScript

という2つのスクリプトがあるとします。

main.jsで、

const worker = new Worker("worker.js");

と記述するだけで、バックグラウンドスレッドが起動され、worker.js のスクリプトが実行されます。

ブラウザは、この API が発行されると、

  • Worker用の実行環境を作成する
  • worker.jsを読み込む
  • JavaScriptを実行する

という処理を行います。

■ Web Worker の停止

Web Worker の停止は、メインスレッド、Web Worker 自身のいづれからも行うことが可能です。

メインスレッドからの停止は、開始と同様に、Web API を使い、Web Worker オブジェクトを停止するだけです。

const worker = new Worker("worker.js");
    :
worker.terminate();

この API を受け取ると、ブラウザは、Web Worker をできるだけ速やかに終了します。

※ Web Worker が、処理途中であっても、その時点で停止してしまうので、注意が必要です。処理の区切りで停止したい場合などは、後述の Web Worker 自身による終了を行う必要があります。

Web Worker 自身で終了を行う場合は、処理完了などの区切りの良いタイミングで、

self.close();

を実行するだけです。

後述のメッセージの送受信と組み合わせることで、メインスレッドからの終了の要求に応じて、区切りの良いタイミングで終了させるということが実現可能です。

self.close(); 以降に書かれたコードは実行されないので、注意が必要です。


メインスレッドと Web Worker のメッセージ送受信

Web Worker は、メインスレッドから起動されます。その為、当然のように、メインスレッドと Web Worker は、オブジェクトや変数を共有しているように見えますが、通常は、どちらも許可されていません。

というのも、Web Worker は、バックグラウンドスレッドで稼働しており、メインスレッドとは全く別の JavaScript 実行環境だからです。そのため、メモリ空間も全くの別ものとなります。

では、メインスレッドと Web Worker は連携することはできないのでしょうか?

そんなことはありません。メインスレッドと Web Worker はメッセージを使った通信により、お互いデータを受け渡したり、連絡を取り合うことが可能です。

その仕組みとして postmessage() が存在します。

postMessage() では、

  • メインスレッド ⇒ Web Worker
  • Web Worker ⇒ メインスレッド

双方向でのメッセージの送受信が可能です。

例:

main.js

const worker = new Worker("worker.js"); // Web Worker 起動

worker.onmessage = (event)=>{
    console.log(event.data);
}
worker.postMessage("こんにちは"); // Web Worker にメッセージ送信

worker.js

self.onmessage = (event)=>{ // メインスレッドからのメッセージ受信
  console.log(event.data);
  self.postMessage("受け取りました"); // メインスレッドにメッセージ送信
}

出力

こんにちは       ・・・ Web Workerが出力
受け取りました   ・・・ メインスレッドが出力

メッセージで送れるもの

postMessage() では、下記のように、多くのもが送付可能です。

  • オブジェクト
  • 配列
  • ArrayBuffer(コピー)
  • JSON
  • 文字列
  • 数値
  • Boolean
  • Map
  • Set

送付されたデータは、基本的には参照ではなく、コピーしたものが渡されます。

👉 これを 構造化複製 と呼びます。

例えば

main.js

const obj = {
    value:10
};
const worker = new Worker("worker.js");

worker.postMessage(obj);

obj.value = 100;

worker.js

self.onmessage = (event)=>{
  console.log(event.data);
}

出力

10

👉 Web Worker には、送信した時点のオブジェクトがコピーされて渡されます。

これは、Structured Clone Algorithm という仕組みです。

※ ただし、ArrayBufferなど一部のオブジェクトはコピーではなく「所有権の移動(Transfer)」もできます。これについては次回、「モダンJavaScript入門/Web Worker の使い方②」で説明します。


Web Worker 起動直後にメッセージ送信しても大丈夫なのか?

もし、Web Worker が onmessage() を登録する前に postMessage() したらそのメッセージはどうなるのでしょうか?

結論から言うと、メッセージは欠落せずに、Web Worker に届きます。

例えば、

メインスレッドで Web Workerを起動し、直後にメッセージを送信したとします。

const worker = new Worker("worker.js");

worker.postMessage("Hello");

この時、 Web Worker が、まだ読み込み中、もしくは、まだ onmessage() を設定していなかった場合、

メインスレッド
  │ postMessage()
  ▼
Web Worker

まだ受信準備できていないとなり、 「メッセージが消えてしまうのでは?」 という疑問がわきます。

では、実際にはどうなるのでしょうか?

実際には、ブラウザが Web Worker 用のメッセージキューを持っており、それが緩衝役となり、

   メインスレッド
     │ postMessage()
     ▼
┌─ Workerのメッセージキュー
│
│   Web worker
│     worker.js初期化
│     onmessage設定
└───▶ Workerのメッセージキューから順番に取り出して処理

メッセージの欠落を防止します。

例えば、メインスレッドで、Web Worker を起動し、A , B , C の順でメッセージを送信したとします。

const worker = new Worker("worker.js");

worker.postMessage("A");
worker.postMessage("B");
worker.postMessage("C");

Web Worker がまだ、self.onmessage = ... を登録していない。もしくは、まだ worker.js をダウンロード途中だったとしても、内部では、Web Worker のメッセージキューに、

[A] [B] [C]

と並びます。その後、Web Worker にて、

self.onmessage = (event) => {
    console.log(event.data);
};

が登録されると、Web Worker のメッセージキューから A , B , C が順番に取り出され、処理されます。

たとえ、Web Worker の初期化で、

// 重い初期化

for(let i=0;i<1000000000;i++){

}

// ここでやっと設定

self.onmessage = ...

のような重い初期化が行われたとしても、それが終わるまでは、メッセージはキューに溜まっています。

だから欠落しません。

ただし、ここで注意が必要です。

もし、 Web Worker が、throw new Error("初期化失敗"); などのエラーで初期化中に、終了してしまった場合、キューに残っていたメッセージは Web Worker 自体が終了するので処理されません。

これは JavaScript のイベントループと同じ考え方です。

例えば

button.addEventListener(...)

このように、postMessage() は単なる関数呼び出しではなく、ブラウザ内部のメッセージキューを介した非同期通信として実現されています。

そのため、Web Worker の起動直後にメッセージを送信しても、通常はメッセージが失われることはありません。

(つづく)