JavaScriptでマルチスレッドを活用!3つの使い方と10個のサンプルコード – JPSM

JavaScriptでマルチスレッドを活用!3つの使い方と10個のサンプルコード

JavaScriptでマルチスレッドを活用するイメージ図JS
この記事は約18分で読めます。

 

【サイト内のコードはご自由に個人利用・商用利用いただけます】

このサービスは複数のSSPによる協力の下、運営されています。

この記事では、プログラムの基礎知識を前提に話を進めています。

説明のためのコードや、サンプルコードもありますので、もちろん初心者でも理解できるように表現してあります。

また、理解しにくい説明や難しい問題に躓いても、JPSMがプログラミングの解説に特化してオリジナルにチューニングした画面右下のAIアシスタントに質問していだければ、特殊な問題でも指示に従い解決できるように作ってあります。

基本的な知識があればカスタムコードを使って機能追加、目的を達成できるように作ってあります。

※この記事は、一般的にプロフェッショナルの指標とされる『実務経験10,000時間以上』を凌駕する現役のプログラマチームによって監修されています。

サイト内のコードを共有する場合は、参照元として引用して下さいますと幸いです

※Japanシーモアは、常に解説内容のわかりやすさや記事の品質に注力しております。不具合、分かりにくい説明や不適切な表現、動かないコードなど気になることがございましたら、記事の品質向上の為にお問い合わせフォームにてご共有いただけますと幸いです。
(送信された情報は、プライバシーポリシーのもと、厳正に取扱い、処分させていただきます。)

はじめに

この記事を読めば、JavaScriptでマルチスレッドを活用することができるようになります。

JavaScriptのマルチスレッド処理は、Web Workersを使って実現されます。

この記事では、Web Workersの基本から使い方、注意点、カスタマイズ方法まで、初心者向けに徹底解説します。

また、実践的なサンプルコードも10個紹介するので、ぜひ参考にしてみてください。

●JavaScriptでのマルチスレッドとは

JavaScriptはもともとシングルスレッドで動作する言語ですが、Web Workersを使うことでマルチスレッド処理を実現することができます。

マルチスレッド処理により、複数のタスクを並行して処理することが可能となり、パフォーマンスの向上が期待できます。

○Web Workersの基本

Web Workersは、JavaScriptのメインスレッドとは別にバックグラウンドで動作するスレッドを生成することができる機能です。

これにより、重たい処理をバックグラウンドで実行させることで、UIのフリーズを防ぐことができます。

●マルチスレッドの使い方

では、実際にWeb Workersを使用してマルチスレッド処理を実現する方法を見ていきましょう。

下記のサンプルコードでは、単純な計算や画像処理、Ajaxリクエストを並列処理する例を紹介します。

○サンプルコード1:単純な計算を並列処理

まずは、単純な計算を並列処理する例を見ていきましょう。

下記のサンプルコードでは、1から10000までの数字を足し合わせる処理を行っています。

【メインスレッド側のコード】

// worker.jsを読み込む
const worker = new Worker("worker.js");

// メッセージを受け取った時の処理
worker.addEventListener("message", (event) => {
  console.log("Result:", event.data);
});

// 処理を開始するメッセージを送信
worker.postMessage({ start: 1, end: 10000 });

【worker.js(Web Worker側のコード)】

// メッセージを受け取った時の処理
self.addEventListener("message", (event) => {
  const { start, end } = event.data;
  let sum = 0;

  // 1から10000までの数字を足し合わせる
  for (let i = start; i <= end; i++) {
    sum += i;
  }

  // 結果をメインスレッドに送信
  self.postMessage(sum);
});

○サンプルコード2:画像処理を並列処理

次に、画像処理を並列処理する例を見ていきます。

下記のサンプルコードでは、画像のグレースケール変換を行っています。

【メインスレッド側のコード】

const img = new Image();
img.src = "path/to/image.jpg";
img.onload = () => {
  const canvas = document.createElement("canvas");
  const ctx = canvas.getContext("2d");
  canvas.width = img.width;
  canvas.height = img.height;
  ctx.drawImage(img, 0, 0);

  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

  // worker.jsを読み込む
  const worker = new Worker("worker.js");

  // メッセージを受け取った時の処理
  worker.addEventListener("message", (event) => {
    // グレースケール変換後の画像データを受け取る
    const grayScaleImageData = event.data;
    ctx.putImageData(grayScaleImageData, 0, 0);
  });

  // 画像データをWeb Workerに送信
  worker.postMessage(imageData);
};

【worker.js(Web Worker側のコード)】

// メッセージを受け取った時の処理
self.addEventListener("message", (event) => {
  const imageData = event.data;
  const { data, width, height } = imageData;

  // グレースケール変換を行う
  for (let i = 0; i < data.length; i += 4) {
    const avg = (data[i] + data[i + 1] + data[i + 2]) / 3;
    data[i] = data[i + 1] = data[i + 2] = avg;
  }

  // 結果をメインスレッドに送信
  self.postMessage(imageData);
});

○サンプルコード3:Ajaxリクエストを並列処理

最後に、Ajaxリクエストを並列処理する例を見ていきます。

下記のサンプルコードでは、複数のURLからデータを取得する処理を並行して行っています。

【メインスレッド側のコード】

const urls = ["url1", "url2", "url3"];

// worker.jsを読み込む
const worker = new Worker("worker.js");

// メッセージを受け取った時の処理
worker.addEventListener("message", (event) => {
  const { url, data } = event.data;
  console.log(`Data from ${url}:`, data);
});

// URLをWeb Workerに送信
urls.forEach((url) => {
  worker.postMessage({ url });
});

【worker.js(Web Worker側のコード)】

// メッセージを受け取った時の処理
self.addEventListener("message", async (event) => {
  const { url } = event.data;

  // Ajaxリクエストを行う
  const response = await fetch(url);
  const data = await response.json();

  // 結果をメインスレッドに送信
  self.postMessage({ url, data });
});

これで、Ajaxリクエストを並列処理する方法が分かりました。

次に、注意点と対処法について説明します。

●注意点と対処法

  1. DOM操作はWeb Worker内で行えません。DOM操作はメインスレッド側で行う必要があります。
  2. メインスレッドとWeb Worker間でデータのやり取りをする際には、データのコピーが発生します。
    データ量が大きい場合、パフォーマンスに影響が出る可能性があります。
    SharedArrayBufferやTransferable Objectsを使用してデータのコピーを抑える方法があります。

次に、カスタマイズ方法について見ていきます。

●カスタマイズ方法

  1. データ送受信のカスタマイズ
  2. エラーハンドリングのカスタマイズ

それぞれのカスタマイズ方法について、サンプルコードを用意しました。

○サンプルコード4:データ送受信のカスタマイズ

下記のサンプルコードでは、Transferable Objectsを使って、データをコピーせずに効率的に送受信する方法を表しています。

【メインスレッド側のコード】

const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);

// データをセット
view.setInt32(0, 100);
view.setInt32(4, 200);

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

worker.addEventListener("message", (event) => {
  const { buffer } = event.data;
  const view = new DataView(buffer);

  // 受信したデータを表示
  console.log(view.getInt32(0), view.getInt32(4));
});

// データをWeb Workerに送信(Transferable Objectsを使用)
worker.postMessage({ buffer }, [buffer]);

【worker.js(Web Worker側のコード)】

self.addEventListener("message", (event) => {
  const { buffer } = event.data;
  const view = new DataView(buffer);

  // データを加工
  view.setInt32(0, view.getInt32(0) * 2);
  view.setInt32(4, view.getInt32(4) * 2);

  // データをメインスレッドに送信(Transferable Objectsを使用)
  self.postMessage({ buffer }, [buffer]);
});

○サンプルコード5:エラーハンドリングのカスタマイズ

下記のサンプルコードでは、Web Worker内でエラーが発生した場合のエラーハンドリングをカスタマイズする方法を表しています。

【メインスレッド側のコード】

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

// エラーが発生した場合の処理
worker.addEventListener("error", (event) => {
  console.error("Web Worker error:", event.message);
});

worker.postMessage("Hello, Worker!");

【worker.js(Web Worker側のコード)】

self.addEventListener("message", (event) => {
  // わざとエラーを発生させる
  throw new Error("エラーが発生しました");
});

これで、データ送受信のカスタマイズとエラーハンドリングのカスタマイズの方法が分かりました。

●応用例とサンプルコード

次に、応用例とサンプルコードを見ていきましょう。

○サンプルコード6:複数のWeb Workersを組み合わせる

下記のサンプルコードでは、2つのWeb Workersを組み合わせて、異なる処理を同時に実行する方法を表しています。

【メインスレッド側のコード】

// Web Worker 1
const worker1 = new Worker("worker1.js");

worker1.addEventListener("message", (event) => {
  console.log("Worker 1の結果: ", event.data);
});

worker1.postMessage("Worker 1へのデータ");

// Web Worker 2
const worker2 = new Worker("worker2.js");

worker2.addEventListener("message", (event) => {
  console.log("Worker 2の結果: ", event.data);
});

worker2.postMessage("Worker 2へのデータ");

【worker1.js(Web Worker 1のコード)】

self.addEventListener("message", (event) => {
  const data = event.data;

  // ここで何らかの処理を行う
  const result = `Worker 1が受け取ったデータ: ${data}`;

  self.postMessage(result);
});

【worker2.js(Web Worker 2のコード)】

self.addEventListener("message", (event) => {
  const data = event.data;

  // ここで何らかの処理を行う
  const result = `Worker 2が受け取ったデータ: ${data}`;

  self.postMessage(result);
});

○サンプルコード7:Web WorkersとPromiseを組み合わせる

下記のサンプルコードでは、Web WorkersとPromiseを組み合わせて、非同期処理を扱いやすくする方法を表しています。

【メインスレッド側のコード】

function runWorker(workerUrl, data) {
  return new Promise((resolve, reject) => {
    const worker = new Worker(workerUrl);

    worker.addEventListener("message", (event) => {
      resolve(event.data);
    });

    worker.addEventListener("error", (event) => {
      reject(event.message);
    });

    worker.postMessage(data);
  });
}

(async () => {
  try {
    const result = await runWorker("worker.js", "データを送信");
    console.log("結果: ", result);
  } catch (error) {
    console.error("エラー: ", error);
  }
})();

【worker.js(Web Worker側のコード)】

self.addEventListener("message", (event) => {
  const data = event.data;

  // ここで何らかの処理を行う
  const result = `受け取ったデータ: ${data}`;

  self.postMessage(result);
});

○サンプルコード8:Web WorkersとSharedArrayBufferを使った高速処理

下記のサンプルコードでは、Web WorkersとSharedArrayBufferを使って、高速なデータ処理を行う方法を表しています。

【メインスレッド側のコード】

// バッファのサイズを定義
const bufferSize = 10;

// SharedArrayBufferを作成
const sharedArrayBuffer = new SharedArrayBuffer(bufferSize * Int32Array.BYTES_PER_ELEMENT);

// Int32Arrayを作成
const intArray = new Int32Array(sharedArrayBuffer);

// Web Workerを作成
const worker = new Worker("worker.js");

// メッセージ受信時の処理
worker.addEventListener("message", (event) => {
  console.log("メインスレッドが受け取ったデータ: ", intArray);
});

// Web WorkerにSharedArrayBufferを送信
worker.postMessage({ sharedArrayBuffer });

// データをセット
intArray[0] = 1;
intArray[1] = 2;

【worker.js(Web Worker側のコード)】

self.addEventListener("message", (event) => {
  const { sharedArrayBuffer } = event.data;

  // SharedArrayBufferからInt32Arrayを作成
  const intArray = new Int32Array(sharedArrayBuffer);

  // ここで何らかの処理を行う
  intArray[2] = intArray[0] + intArray[1];

  self.postMessage("処理が完了しました");
});

○サンプルコード9:Web WorkersとOffscreenCanvasを組み合わせる

下記のサンプルコードでは、Web WorkersとOffscreenCanvasを使って、バックグラウンドでCanvasの描画処理を行う方法を表しています。

【HTMLファイル】

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Web WorkersとOffscreenCanvasのサンプル</title>
</head>
<body>
  <canvas id="canvas" width="300" height="300"></canvas>
  <script src="main.js"></script>
</body>
</html>

【main.js(メインスレッド側のコード)】

const canvas = document.getElementById("canvas");
const offscreenCanvas = canvas.transferControlToOffscreen();
const worker = new Worker("worker.js");

worker.postMessage({ offscreenCanvas }, [offscreenCanvas]);

【worker.js(Web Worker側のコード)】

self.addEventListener("message", (event) => {
  const { offscreenCanvas } = event.data;

  // 描画コンテキストを取得
  const ctx = offscreenCanvas.getContext("2d");

  // ここで何らかの描画処理を行う
  ctx.fillStyle = "red";
  ctx.fillRect(10, 10, 100, 100);
});

○サンプルコード10:Service Workersでのマルチスレッド処理

下記のサンプルコードでは、Service Workersを使用してマルチスレッド処理を行う方法を表しています。

この例では、キャッシュを利用してオフラインでもアクセス可能なWebアプリケーションを構築しています。

【HTMLファイル】

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>Service Workersのサンプル</title>
</head>
<body>
  <h1>Service Workersでのマルチスレッド処理</h1>
  <script src="main.js"></script>
</body>
</html>

【main.js(メインスレッド側のコード)】

// サービスワーカーを登録
if ("serviceWorker" in navigator) {
  navigator.serviceWorker
    .register("service-worker.js")
    .then(() => {
      console.log("サービスワーカーが登録されました");
    })
    .catch((error) => {
      console.error("サービスワーカーの登録に失敗しました:", error);
    });
}

【service-worker.js(Service Worker側のコード)】

// キャッシュ名とキャッシュ対象のファイルを定義
const CACHE_NAME = "my-cache";
const CACHE_FILES = ["index.html", "main.js"];

// インストール時のキャッシュ処理
self.addEventListener("install", (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      console.log("キャッシュが開かれました");
      return cache.addAll(CACHE_FILES);
    })
  );
});

// フェッチ時のキャッシュ処理
self.addEventListener("fetch", (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      // キャッシュがあればそれを返す、なければネットワークから取得
      return response || fetch(event.request);
    })
  );
});

このサンプルコードを使用することで、Service Workersを利用したマルチスレッド処理を実現することができます。

これにより、Webアプリケーションのパフォーマンス向上やオフライン対応が可能になります。

まとめ

Web WorkersやService Workersを使って、マルチスレッド処理を行うことで、Webアプリケーションのパフォーマンスを向上させることができます。

また、PromiseやSharedArrayBuffer、OffscreenCanvasなどの技術を組み合わせることで、さらに効率的な処理が可能になります。

これらのサンプルコードを参考に、Webアプリケーションの開発が向上すれば嬉しい限りです。