Web Workerの使い方 | JavaScriptの並列化を体験しよう

Web Workerとは、一部のスクリプトをメインスレッドから別のスレッドに分離し、UIの描画を阻害せずに処理を行うことができる仕組みです。

JavaScriptはかつて完全にシングルスレッドの言語で、UIを描画するメインスレッドで全ての処理を行っていました。計算負荷の高い処理を行うとページがフリーズしてしまうため、重い処理はできないのが常識でした。

Web Workerを使用すれば、非同期処理では対応できない重い処理も並列化して別スレッドで実行できます。

JavaScriptのコールバック関数と非同期処理を丁寧に理解する

本記事では触って確かめられる実例も交えて、Web Workerの基本の使い方を紹介します。

Web Workerの使い方

Web Worker APIはシンプルなAPIで、メインスレッドで動作するスクリプトと、ワーカースレッドで動作するスクリプトの間でメッセージングを行い使用します。

必要なファイルを用意しよう

まず、以下のようにJavaScriptファイルと、HTMLファイルを用意しましょう。

.
└── index.html/
└── js/
├── main.js
└── worker.js
  • index.html:ワーカーを使用するページのHTML
  • js:JSファイルを入れておくディレクトリ
  • main.js:メインスレッドで動作する通常のJS
  • worker.js:Web Worker用のJS

メインスレッドのスクリプトを書く

最初に、メインスレッドで動作するmain.jsで、Web Workerを使用することを宣言します。

{"filename":"main.js","code":"\/\/ Worker\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u3092\u4f5c\u6210\nvar worker = new Worker('.\/js\/worker.js');","language":"javascript","id":1}

‘ . / js / worker.js ‘:Worker()に指定するファイルの場所は、HTMLファイルからみた位置(パス)で指定します。

次に、main.jsからworker.jsにメッセージを送り、同時にworker.jsからのレスポンス(返答)を待機します。

以下はメインスレッドからワーカースレッドに挨拶(’greeting’)を要求する、Web Workerの最も簡単な例です。

{"filename":"main.js","code":"\/\/ Worker\u30a4\u30f3\u30b9\u30bf\u30f3\u30b9\u3092\u4f5c\u6210\nvar worker = new Worker('.\/js\/worker.js');\n\/\/ \u4e8b\u524d\u306b\u3001\u30ef\u30fc\u30ab\u30fc\u304b\u3089\u306e\u30ec\u30b9\u30dd\u30f3\u30b9\u3092\u5f85\u6a5f\nworker.addEventListener('message', function(e){\n  \/\/ \u30ef\u30fc\u30ab\u30fc\u304b\u3089\u9001\u3089\u308c\u3066\u304d\u305f\u30ec\u30b9\u30dd\u30f3\u30b9\u3092\u53d7\u3051\u53d6\u308b\n  \/\/ \u30ec\u30b9\u30dd\u30f3\u30b9\u306e\u5185\u5bb9\u306f\u3001e.data\u306b\u5165\u3063\u3066\u3044\u308b\n  const data = e.data;\n  switch(data.command){\n    case 'greeting':\n      console.log(`From the Worker: ${data.res}`);\n      break;\n    default:\n      console.log('Error!');\n  }\n}, false);\n\/\/ \u30ef\u30fc\u30ab\u30fc\u306b\u30e1\u30c3\u30bb\u30fc\u30b8\u3092\u9001\u308b\nworker.postMessage(\n  {'command': 'greeting'}\n);","language":"javascript","id":1}

メインスレッドとワーカースレッドでやり取りするメッセージはJavaScriptのオブジェクトで作り、postMessage()で送ります。

好きなプロパティをいくつでも設定できますが、ここでは、commandプロパティにメッセージの種類('greeting')を設定しています。

メインスレッド側では(ワーカーからいつレスポンスが来ても良いように)イベントリスナーで事前に待機処理を記述しておかなければなりません。

レスポンスの待機で、switch ... case文を用いているのは、複数のやり取り(メッセージング)を追加した際に、ワーカーからのレスポンスの種類を正しく認識するためです。

ポイント
  • JSオブジェクトをpostMessage()で送る
  • メッセージの中身はe.dataから取り出す
  • イベントリスナーで、レスポンスに対する待機処理を事前に記述する

ワーカースレッドのスクリプトを書く

次に、ワーカースレッドで動作するworker.jsを書いていきます。

イベントリスナーでmain.jsからのメッセージに待機します。

{"filename":"worker.js","code":"self.addEventListener('message', (e) => {\n  \/\/ \u9001\u3089\u308c\u3066\u304d\u305f\u30e1\u30c3\u30bb\u30fc\u30b8\u306e\u5185\u5bb9\u306f\u3001e.data\u304b\u3089\u53d6\u308a\u51fa\u3059\n  const data = e.data;\n  \n  switch(data.command){\n    \/\/ 'greeting'\u30b3\u30de\u30f3\u30c9\u3060\u3063\u305f\u3089\u3001\u6328\u62f6\u3092\u8fd4\u3059\n    case 'greeting':\n      self.postMessage({\n        'command': 'greeting',\n        'res' : \"Hello, Main Thread. I'm a Web Worker.\"\n      });\n      break;\n  };\n}, false);","language":"javascript","id":1}

基本の処理はmain.jsとほぼ同じです。main.jsからメッセージが受け取った時にpostMessage()でレスポンスしている部分だけ注意しましょう。

ここでは、commandオプションが'greeting'のメッセージだったら、レスポンスとして、"Hello, Main Thread. I'm a Web Worker."と挨拶を返すようにしています。

以上で、Web Workerによる並列化処理は完了です。

本当にわずかな時間ですが、メインスレッドからメッセージが送られてからワーカーが挨拶を返すまで、そのやり取りに要する内部処理はメインスレッドから分離されています。

実用上は、Web Workerで分離したいのは挨拶ではなく「重い処理」です。次章では実際にその違いを確認しましょう。

Web Workerの効果を確認しよう

本章では、Web Workerを使用した時と使用しなかった時の違いを実例で確認してみましょう。

デモページを触ってみよう

「重い処理」をメインスレッドで無理やり実行した時と、Web Workerに分離した場合の違いが分かるページを作りました。

「重い処理」は次節に記載しますが、誤解を恐れずざっくり言うと、決められたコストの中で最も良い買い物をするような計算をしています。

フォームに2桁か3桁の数字(コスト)を入力して、「In UI Thread」のボタンを押してみてください。すると、計算中は明らかにページがフリーズし、ストップウォッチが停止してしまいます。

一方で、「In Web Worker」のボタンを押してみてください。同じ処理をしていてもワーカースレッドで動くため、アニメーションが止まらないことが分かりますね。

デモページのソースコードはGitHubにあります。

デモページの「重い処理」

余談ですが、デモページの「重い処理」がなぜ重いのか記載しておきます。現実世界で、同じ計算を実装することは無いので、詳細が気にならない人は飛ばしてください。

買い物の計算という例え話を正確に言い直すと、実際に裏側で動いている処理は、「最適化されていないナップサック問題」です。

ナップサック問題とは、以下のような有名問題です。

ナップサック問題

N個の商品があり、各商品にはそれぞれコストwと価値vが割り当てられている。

選んだ商品のコストの合計total(w)が制約Wを超えないような選び方のうち、選んだ商品の価値total(v)が最大となる選び方を解答してください。

デモページの「FULL GOODS LIST」ボタンを押すと、商品の一覧が見られます。その例で説明すると、

  • 制約Wは、フォームに入力した「ショッピングカートの大きさ」
  • コストwは、各商品のSize
  • 価値vは、各商品のValue

となります。

「商品はいくつ選んでもいいが、価値の合計total(v)を最大にしなければならない」というところがポイントで、仮に愚直に計算しようとすると、

  • 商品1を選ぶ or 選ばない → 2通り
  • 商品2を選ぶ or 選ばない → 2通り

  • → 2N通り

を全て調べないと解答できません。2N通りは膨大な数で、商品が10個でも1024通りあり、デモページの例だと3300万通り程度あります。

選び方全てに対して、1つ1つコストの合計、価値の合計を計算しているので、「重い処理」となるわけですね。

動的計画法(DP)
実はナップサック問題には計算量を大幅に削減できる動的計画法が知られています。アルゴリズムを解説する記事では無いので省きますが

実用上、Web Workerはいつ使われるのか

実用上JavaScriptでWeb Workerが使用される例は主に以下の3つでしょう。

  • グラフィック処理(リッチなWeb表現)
  • データやモデル等、重いファイルの読み込み
  • 外部APIへの接続

グラフィック処理は、幾何学模様や3DモデルなどのリッチなWeb表現を描画する処理です。

2番目に関しては、特に昨今重要なのがAIモデルのローディングです。AIモデルはサイズが大きい傾向にあるので、ローディングはネックになりやすいところです。

3番目は、Webアプリやサイトだけでなく外部の環境に依存するため、時間がかかるかかからないかを事前に予測するのが難しい例ですね。時間がかかった時にメインスレッドを阻害したくない場合に利用します。

Web Workerの制限

便利なWeb Workerですが、知っておかなければならない制約が2点あります。

  • ワーカースレッドからは、DOM要素を操作できない
  • 同じ変数への同時アクセスは不可

DOM要素の操作はメインスレッドから

JSの役割は急速に拡大していますが、今も昔もDOM要素の管理が最も重要な役割の1つであることに変わりはありません。

そのため、DOM要素の操作はメインスレッドのみに許可されており、ワーカースレッドに委任することはできません。

例えば、以下のように、メインスレッドで何か動かしている間に、ワーカースレッドでボタンの色を変更することは出来ません。

{"filename":"main.js","code":"var worker = new Worker('.\/js\/worker.js');\nworker.addEventListener('message', function(e){\n  const data = e.data;\n  switch(data.command){\n    case 'change_login_button_color':\n      console.log(`From the Worker: ${data.res}`);\n      break;\n    default:\n      console.log('Error!');\n  }\n}, false);\n\/\/ \u30dc\u30bf\u30f3\u8272\u3092\u5909\u66f4\u3059\u308b\u3088\u3046\u6307\u793a\u3092\u51fa\u3059\nworker.postMessage(\n  {'command': 'change_login_button_color'}\n);","language":"javascript","id":1}
{"filename":"worker.js","code":"self.addEventListener('message', (e) => {\n  const data = e.data;\n  \n  switch(data.command){\n    \/\/ 'change_button_color'\u30b3\u30de\u30f3\u30c9\u3060\u3063\u305f\u3089\u3001\u30ed\u30b0\u30a4\u30f3\u30dc\u30bf\u30f3\u306e\u8272\u3092\u5909\u66f4\u3059\u308b\n    case 'change_login_button_color':\n      const login_button = document.querySelector('#loginBtn');\n      login_button.style.color = 'green';\n      self.postMessage({\n        'command': 'change_login_button_color',\n        'res' : \"login button color changed.\"\n      });\n      break;\n  };\n}, false);","language":"javascript","id":1}

一見不便ですが、仮にDOM要素の操作を並列化できた場合、メインスレッドとワーカースレッドで同時に同じボタンの色を変更することもできてしまいそうです。

すると、時にはボタン色が赤で、別の時にはボタン色が緑、みたいなことが起こってしまう恐れがあり危険です。JSの性質を考えると、実はもっともな制約です。

同じ変数への同時アクセス不可 | スレッドセーフ

Web Workerは特定の処理をメインスレッドから分離しますが、メインスレッドとワーカースレッドの同じ変数への同時アクセスは不可になっています。

例えば、100個の数字が1つの配列に格納されていて、100個の数字の合計を求めたいとします。

メインスレッドとワーカースレッドで同時に同じ配列にアクセスして、メインスレッドでは前半の50個、ワーカースレッドで後半の50個を同時に足し算して、後で結果を集計する、と言う処理はできません。

正確には、同時に計算できないわけではなく、後半の50個のデータのコピーを作成して、メインスレッドからワーカースレッドに渡す必要があります。

並列化を知っている方なら指摘したくなる部分でしょうが、Web Workerによる並列化は、マルチスレッドというよりむしろマルチプロセス化と言った方が正しいのかも知れません。

まとめ

以上でWeb Workerの基本の使い方と知識についての説明を終わります。

最後に、ポイントをまとめておきますので参照してください。お疲れ様でした。

ポイント
  • メインスレッド用のJSとワーカースレッド用のJ’Sを用意する
  • メッセージはJSオブジェクトで送り、イベントリスナでレスポンスを待機する
  • 実用上は、グラフィックスやAIモデルのロードなどに使われる
  • JSの役割や仕様に基づく並列化の制約に注意する

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA


error: Content is protected !!