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

前回はWebWorkerの中でも最もシンプルに並列処理を扱うことができる「専用ワーカー (Dedicated Worker)」について取り上げました。今回はiframeや他のウィンドウ(タブ)などから起動中のWorkerを利用することのできる「共有ワーカー(Shared Worker)」について取り上げます。

ページが違えど処理は共通と言った場合に利用すれば、クライアントのリソースの消費を抑えることが可能です。

共有ワーカーの基本的な原理

イメージ

途中までは専用ワーカーと同様ですが、作成したWokerは他のスレッドからも利用することが可能です。

プロセスの状態

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

ここでは共有ワーカーを利用するタブを3つほど開いてみました。最初にWorkerを起動したタブの中にスレッドが作成されこれを他のタブから利用する作りのようです。

ちなみに最初のタブを閉じた場合どうなるかと言うと、タブその物のプロセスは終了しますがWorkerだけ裏側で生き残り続けるようです。

共有ワーカーを利用しているタブをすべて閉じるとWorkerも自動的に終了します。

※これはあくまでGoogleChromeの実装です。ブラウザによっては異なる動きを裏側している可能性があります。

ブラウザの対応状況

執筆時点での「Shared Worker」の各ブラウザの対応状況はご覧の通りです。

Can I useより

スマホでよく利用されているブラウザは未対応、PCもIEは当然としてSafariも未対応となっています。Safariのやる気がないのは今に始まったことではないので、生暖かい目で見守って上げてください。

いやー、やる気なくなりましたねw ブラウザに制約がかけられない場合、当面は専用ワーカーで凌ぐしかないですね。

使い方

解説は専用ワーカーとの違いを中心に書いていきます。

メインスレッド側の処理

共有ワーカーを利用する際のコードは以下の通りです。

// Workerの作成と開始
const myWorker = new SharedWorker("worker.js");
myWorker.port.start();

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

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

専用ワーカーとの違いはそれほど多くありません。具体的には以下の3つです。

  1. インスタンスを生成する際に SharedWorker を用いる
  2. インスタンス作成後に start()メソッドを実行する
  3. メッセージを送る/受け取る場合は port を挟む

共有ワーカーは接続してくるプロセスを区別するために「ポート」を利用しているというわけです。

Worker側の処理

Worker側のコードは専用ワーカーと比べて少し仕事が増えています。

/*
 * worker.js
 */

// メインスレッドが接続
self.addEventListener("connect", (e) => {
  const port = e.ports[0];

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

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

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

  // スレッドの処理を開始
  port.start();
});

サーバのようにプロセスが「接続」してくることを考慮する必要があるため、最初に行うのはconnectイベントを拾うことです。接続したあとはメッセージを受け取ったのをトリガーとして処理を記述できますが、このときのイベントはportに対して紐付ける必要があります。最後にメインスレッドと同様にこちらもstart()しておく必要があります。

[例] 文字の順番を反転するWebWorker^2

前回のサンプルを応用した物です。複数のタブやウィンドウで実行し、ブラウザ(Chrome)のタスクマネージャーなどで動きを観察してみてください。

実行例

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

  • 次の2つのページを同時にiframeで読み込んでいます。
    1. テキストボックスに文字を入力する度に、順番が反転した文字が下部に表示される
    2. ラジオボタンを選択する度に、固定文が反転して下部に表示される
  • 文字を反転する処理を上記の2つとも同じスレッド(SharedWorker)で実行しています。

ソースコード

index.html

このファイルはiframeで2つのファイルを呼んでいるだけです。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>WebWorker Sample2a</title>
</head>
<body>

<h1>WebWorker Sample2</h1>

<iframe src="frame1.html" width="350" height="180"></iframe>
<iframe src="frame2.html" width="350" height="180"></iframe>

</body>
</html>

frame1.html

こっちは前回のサンプルと同じ動作をしてくれます。そのHTML部分。

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

<h1>自由文を反転する</h1>

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

<script src="app1.js"></script>
<script>
document.querySelector("#frm-reverse").addEventListener("submit", e=>e.preventDefault());
</script>

</body>
</html>

frame2.html

前回のサンプルは自由文が入力できましたが、こちらは固定文を反転する仕様です。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>WebWorker Sample2b</title>
  <style>
    h1{ font-size:14pt; }
    label { cursor: pointer; border-bottom: 1px solid gray;}
    #serif { padding:5px; margin:5px; width:250px; font-size:14pt; }
    #response { margin:5px; font-size:14pt; color:Red}
  </style>
</head>
<body>

<h1>固定文を反転する</h1>

<form>
  <input type="radio" id="serif1" name="serif" value="Apple"><label for="serif1">Apple</label>
  <input type="radio" id="serif2" name="serif" value="Banana"><label for="serif2">Banana</label>
  <input type="radio" id="serif3" name="serif" value="Orange"><label for="serif3">Orange</label>

  <div id="response">
    <small style="color:gray">ここに反転した文字が表示されます</small>
  </div>
</form>

<script src="app2.js"></script>

</body>
</html>

app1.js

自由文を反転するメインスレッドのJavaScriptです。

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

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

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


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

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

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

  serif.focus();
}

app2.js

固定文を反転するメインスレッドのJavaScriptです。

const serif = document.querySelectorAll("input[type='radio'][name='serif']");
const response = document.querySelector("#response");

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

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


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

  // ラジオボタンが入力されたら
  for(let i=0; i<serif.length; i++){
    serif[i].addEventListener("change", (e)=>{
      const value = serif[i].getAttribute("value");
      myWorker.port.postMessage(value);      // Workerにメッセージを送信
    });
  }

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

worker.js

こちらが共有ワーカーの本体です。app1.jsとapp2.jsから利用されます。

/*
 * メインスレッドからメッセージ受信
 */
self.addEventListener("connect", (e) => {
  const port = e.ports[0];

  port.addEventListener("message", (e)=>{
    const param = e.data;

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

  port.start();
});

戻る

blog.katsubemakito.net

参考ページ