JavaScriptをマルチスレッドで動かす方法

  • プロダクション
JavaScriptをマルチスレッドで動かす方法

目次

プログラムの実行モデルには、大きく分けて「シングルスレッド」と「マルチスレッド」の二つの概念があります。
JavaScriptは基本的に「シングルスレッド」の言語なのですが、シングルスレッドゆえの問題点があります。

以下がその例です。


重たい処理を実行し、処理の進行度をパーセンテージで画面に表示するサンプルですが、シングルスレッドではカウントアップがされず、ボタンを押してから数秒後に0%から100%に一気にパーセンテージが変化しています。

これは重い計算処理がメインスレッドを占有し、ブラウザがフリーズしてしまっているためです。
JavaScriptはシングルスレッドで動くため、この現象が発生してしまいます。

マルチスレッドのサンプルでは0%から100%まで段階的にカウントアップされています。
期待通りの結果です。

JavaScriptでも「Web Worker」を使うことでマルチスレッドのように動作させることが可能です。
この記事ではシングルスレッドとマルチスレッドの違い、Web Workerを使ってJavaScriptをマルチスレッドで動かす方法について解説します。

シングルスレッドとマルチスレッド

プログラムの実行モデルには、大きく分けて「シングルスレッド」と「マルチスレッド」の二つの概念があります。
この2つの実行モデルにはそれぞれメリットとデメリットが存在します。

シングルスレッドの特徴

メリット: コードの理解がしやすく、デバッグが容易
デメリット: 重たい処理を実行した場合、その間は他の処理が実行できない

シングルスレッドとは、プログラムが一つの処理の流れで順番に命令を実行する方式です。
プログラムのフローが直線的のためコードの理解がしやすく、マルチスレッドと比べてデバッグが容易です。
しかし、一つの処理の実行に長時間かかる場合、その間は他の処理が実行できません。

マルチスレッドの特徴

メリット: 複数のスレッドを扱えるためパフォーマンスが向上する
デメリット: 処理を追いにくく修正やバグの解消が困難、データの不整合が発生する可能性がある

マルチスレッドとは、複数のスレッドを同時に実行することで処理を並列化する方式です。
複数のスレッドが同時に動作するため、一つのスレッドが時間のかかる処理をしていても別のスレッドが他の処理を継続でき、全体のパフォーマンスが向上します。

その反面、複数のスレッドを同時に行うため処理を追うことが難しくなり、バグの発見や修正が困難になったり、複数のスレッドが同じリソースにアクセスする場合、競合やデータの不整合が発生する可能性があります。

JavaScriptはシングルスレッドだが……

JavaScriptは基本的に「シングルスレッド」の言語です。
ただし、JavaScriptでもマルチスレッドのような並列処理を行う方法があります。
それが、「Web Worker」を使用する方法です。
Web Workerを活用した並列処理の実装方法について詳しく説明していきます。

Web Workerとは?

Web Workerは、JavaScriptにおける並列処理を実現するための仕組みです。
バックグラウンドで別のスレッドとして処理を実行することで、メインスレッドの負荷を減らし、UIの応答性を向上させることが可能になります。

主な用途としては下記が挙げられます。
・重い計算処理(例:画像処理、暗号化、データ解析など)
・大規模なデータ処理(例:JSONデータのパース、大量のデータのフィルタリング)

Web Workerの使い方

基本的なコードを紹介します。

Web Worker を作成するためのコンストラクタ。
ここで定義するworker を使ってWeb Workerスレッドとのやり取りをする。

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

メインスレッドから Web Worker にデータを送る。
第一引数のデータをWeb Worker側に送信できる。
worker.postMessage({ message: "Hello, Worker!" });

Web Worker側でメインスレッドからのメッセージを受け取る。
worker.postMessageで送信したデータを第一引数で受け取ることができる。
self.onmessage = function(event) {
  console.log("受け取ったメッセージ:", event.message);
};

メインスレッドから Web Worker を強制終了する。
worker.terminate();

サンプル

今回作成したサンプルのソースコードです。
ボタンをクリックすると1から5千万まで数値を加算し、計算の進行度をパーセンテージで画面に表示するというものです。

メインスレッドとWeb Workerはそれぞれ以下の処理を担当しています。
メインスレッド: ボタンのクリックイベント、Web Workerから受け取ったパーセンテージの表示
Web Worker: 計算処理、計算の進行度をパーセンテージでメインスレッドに送信

index.html(body部分)

<body>
  <div class="wrapper">
    <div class="title">Web Worker</div>
    <button class="execute">重たい処理を実行</button>
    <p class="progress">0%</p>
  </div>
</body>

main.js
const executeButtonElement = document.querySelector(".execute");
const progressElement = document.querySelector(".progress");
const SUM_TO = 50000000;
executeButtonElement.addEventListener("click", async () => {   executeCalculation(); });
function executeCalculation() {   // Workerを作成   const worker = new Worker("./worker.js");
  // Workerにデータを送信   worker.postMessage({ countTo: SUM_TO });
  // Workerから結果を受け取る   worker.onmessage = (event) => {     const { progress } = event.data;
    progressElement.textContent = `${progress}%`;
    if (progress === 100) {       // Workerを終了       worker.terminate();     }   };
  // エラーハンドリング   worker.onerror = function (error) {     console.error("エラー:", error);   }; }

worker.js
self.onmessage = function(event) {
  const { countTo } = event.data;
  let sum = 0;
  for (let i = 1; i <= countTo; i++) {     const progress = Math.floor((i / countTo) * 100);
    if (i % (countTo / 100) === 0) {       self.postMessage({ progress });     }
    sum += i   }
  // メインスレッドに結果を送信   self.postMessage(sum); };
計算のたびにpostMessageを実行すると処理速度が大幅に長くなってしまうため、少し工夫が必要でした。 以下のコードでcountTo の値を100等分し、その区切りごとに postMessage を呼び出しています。
if (i % (countTo / 100) === 0) {
  self.postMessage({ progress });
}

まとめ

フロントエンドで画面が固まるほどの重い処理を扱うことは多くはないため使い所は多くないものの、適切な場面で活用すればパフォーマンス向上やユーザー体験向上に役立つ強力な手段となりそうです。

この記事の執筆者

T.S DXプラットフォーム事業部

この記事に関するご相談やご質問など、お気軽にお問い合わせください。

お問い合わせ

関連する記事

すべての記事を見る

タグ一覧