[HTML5] WebWorkerで並列処理を行う - 専用ワーカー編

JavaScript非同期処理だから、複数同時に処理してるんでしょ?……と思っていた時期がありましたw JavaScriptの非同期処理とは時間のかかる処理の待ち時間で他のことをやってしまおうという物で、雑に言うなら実行する順番をコントロールする仕組みのことです。つまり並列処理とは異なる物。

本来の意味での並列処理を実装するための機能はWebWorkerで実装することができます。シンプルな仕様にまとめられているため簡単にJavaScriptでも並列処理を実現できます。WebWorkerにはいくつか種類があるのですが、今回は「専用ワーカー(Dedicated Workers)」について取り上げます。

WebWorkerの種類

W3Cのドキュメント内で定義されているWebWorkerは次の2種類です。

専用ワーカー (Dedicated Worker)
Workerを作成したメインスレッドからのみ利用されます。この記事で取り扱います。
共有ワーカー (Shared Worker)
Workerを複数のプロセスから利用できます。異なるタブやウィンドウ、iframeなどから起動中のWorkerに処理を任せることができます。詳しくは次回の記事を参照ください。

名前が似ているので同一視されるServiceWorkerさんは、上記のWebWorkerとは毛色の違う仕組みです。

専用ワーカーの基本的な原理

イメージ

並列処理と言うと難しそうなイメージを抱く方も多いと思いますが、実際にはシンプル。大きな流れは以下の通りです。

  1. 並列処理が必要になったタイミングでスレッド(Worker)を新規に作成
  2. メインスレッドからWorkerに「メッセージ」という形で指示を与える
  3. Workerが処理をスタート
  4. Workerの処理が完了したらメインスレッドに「メッセージ」で報告

Workerはこれらのやり取りが終了して以降も存在し続けますので、必要に応じてメインスレッドからメッセージを送ればその度に処理を並列して行うことができます。不要となった時点で終了させることも可能です。

プロセスの状態

GoogleChromeでは以下のように「ハンバーガーメニュー」から「その他のツール」→「タスクマネージャー」とたどることでChromeが実行している全てのプロセスの一覧を確認できます。

実際の一覧は以下です。タブや機能拡張毎にプロセスが立ち上がっているのがわかりますね。今回取り上げる専用ワーカーは、実行しているタブのプロセスの中で生成されます。

この記事の下の方にサンプルがありますので実際に実行してみてくださいませ。

ブラウザの対応状況

執筆時点での「専用ワーカー」の各ブラウザの対応状況はご覧の通りです。

Can I useより

IEも含めてPCもスマホも主要なブラウザではすべて対応済みとなっていますね。安心して使えそうです。

使い方

メインスレッド側の処理

JavaScriptでのWokerは独立したファイルとして用意する必要があります。Workerを作成するにはWorker()インスタンス生成時にこのファイルを引数として指定するだけです。

/*
 * foo.js
 */

// Workerの作成
const myWorker = new Worker("worker.js");

// Workerへメッセージを送信
myWorker.postMessage("Hello MyWebWorker");

// Workerからのメッセージを受信
myWorker.addEventListener("message", (e)=>{
  const param = e.data;    // Workerからの引数を受け取る
});

Workerに「メッセージ」を送信するにはpostMessage()メソッドを実行します。引数として渡せるのは文字列や数字の他に配列やオブジェクト(連想配列)など大抵の物が利用できます。なおメッセージのやり取りは何度でも実行できます。

Workerからの「メッセージ」を受信する際にはmessageイベントが発生しますので、事前にaddEventListener()などで処理を登録しておきます。引数はcallback関数に渡されます。

Worker側の処理

メインスレッドからのメッセージが送られると、Workerファイル内ではmessageイベントが発生します。引数はcalback関数の引数として渡されます。あとはこのイベントをトリガーとして処理を行い、最終的にpostMessage()で結果を返します。メインスレッドと同様に引数としては様々なデータが渡せますし、何度でも実行できます。

/*
 * worker.js
 */

// メインスレッドからのメッセージを受信
self.addEventListener("message", (e) => {
  const param = e.data;  // これでメインスレッドからの情報を受け取れる

  //----------------------------------
  // このあたりにWorkerにさせたい処理を書く
  // (途中経過のメッセージを送信するのもあり)
  //----------------------------------

  // メインスレッドへメッセージを送信
  self.postMessage("Nice to meet you");
});

selfは省略することが可能です。またonmessageイベントハンドラに関数を渡すことでも同様の処理を行うことができます(MDNのサンプルはこちらですね)。

Workerを強制終了

実行中のWorkerを何かしらの理由で強制的に終了させたい場合はterminate()メソッドを実行します。

/*
 * foo.js
 */

// Workerの作成
const myWorker = new Worker("worker.js");

// Workerの強制終了
if( confirm("Workerを強制終了しますか?") ){
  myWorker.terminate();
}

エラー処理

Worker内で例外が発生したことを補足したい場合は、メインスレッド内でerrorイベントを見張っておきます。

/*
 * foo.js
 */

// Workerの作成
const myWorker = new Worker("worker.js");

// エラー処理
myWorker.addEventListener("error", (e)=>{
  console.error(e.message);
});
/*
 * worker.js
 */

// メインスレッドからのメッセージを受信
self.addEventListener("message", (e) => {
  // 例外を発生
  throw "Wow!";
});

エラーオブジェクトには次のような内容が含まれています。

message
人間がパッと見て理解できるエラー内容
filename
エラーが発生したファイル名
lineno
エラーが発生したファイルの行番号

制約

開発者が意図しないような挙動を取ることが懸念されるため、WebWorkerではいくつかの制約があります。

  • DOMをWorker内から直接操作することは出来ない
  • window, document, parentオブジェクトの一部が利用出来ない

[例] 文字の順番を反転するWorker

実際に簡単なサンプルを置いておきます。

実行例

以下から実際に試すことができます。 miku3.net

  • テキストボックスに文字を入力する度に、順番が反転した文字が下部に表示されます。
  • 文字を反転する箇所をWebWorkerで処理しています。

ソースコード

index.html

メインスレッドにあたります。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>WebWorker Sample1</title>
  <style>
    #serif { padding:5px; margin:5px; width:250px; font-size:14pt; }
    #response { margin:5px; font-size:14pt; color:Red}
  </style>
</head>
<body>

<h1>WebWorker Sample</h1>
<p>入力された文字の順番を反転します。</p>

<form>
  <input type="text" id="serif" placeholder="文字を入力してください">
  <div id="response">
    <small style="color:gray">ここに反転した文字が表示されます</small>
  </div>
</form>

<script>
const serif = document.querySelector("#serif");
const response = document.querySelector("#response");

/*
 * [イベント] ページの読み込みが完了
 */
window.onload = ()=>{
  // WebWorkerの対応チェック
  if ( ! window.Worker ) {
    alert("WebWorkerに対応していないブラウザです");
    return(false);
  }

  // WebWorker用の処理をスタート
  startWorker();
}

/**
 * WebWorker用の処理を実行
 *
 * @param {void}
 * @return {void}
 */
function startWorker(){
  // スレッド作成
  const myWorker = new Worker("worker.js");

  // テキストボックスに入力されたら
  serif.addEventListener("keyup", (e)=>{
    myWorker.postMessage(serif.value);    // Workerにメッセージを送信
  });

  // Workerからのメッセージを受信
  myWorker.addEventListener("message", (e)=>{
    response.textContent = e.data;
  });
  // Workerのエラー処理
  myWorker.addEventListener("error", (e)=>{
    console.error(`[myWorker] ${e.message}`);
  });

  serif.focus();
}
</script>
</body>
</html>

worker.js

こちらがWorkerです。

/*
 * メインスレッドからメッセージ受信
 */
self.addEventListener("message", (e) => {
  const param = e.data;

  // メインスレッドにメッセージを返す
  self.postMessage(param.split("").reverse().join(""));
});

続き

blog.katsubemakito.net

参考ページ