🔄

C#erから見たJavaScriptの非同期処理

2024/10/09に公開
6

解法

C# 開発者が JavaScript の非同期プログラミングとマルチスレッド処理を理解するには、以下の主要な違いと類似点に注目することが重要です:

  1. Promise と Task の対応関係
  2. 実行モデルの根本的な違いと並行処理
  3. Promise の then チェーンと await の違い
  4. Web Workers を使用したマルチスレッド処理

解説

Promise と Task の対応関係

C# の Task に相当するのが JavaScript の Promise です。両者とも非同期操作の結果を表現するオブジェクトですが、使い方に違いがあります。

C# の Task の例:

async Task DoAsync()
{
    await Task.Run(() =>
    {
        // 重たい同期処理
    });
}

JavaScript の Promise の例:

async function doAsync() {
    await new Promise(resolve => {
        // 重たい同期処理
        resolve();
    });
}

実行モデルの根本的な違いと並行処理

JavaScript のメインスレッドはシングルスレッドで動作しますが、Web Workers を使用することで並行処理やマルチスレッド処理が可能になります。以下に C# と JavaScript の実行モデルの違いを示します:

特性 C# JavaScript
実行モデル マルチスレッドモデル メインスレッドはシングルスレッド、Web Workers でマルチスレッド可能
並列処理 真の並列処理が可能(Parallelクラスなど) メインスレッドは並行処理、Web Workers で並列処理可能
同期コードのブロッキング 同期メソッドは他のスレッドをブロックしない メインスレッドの長時間実行コードはイベントループをブロック
キャンセレーション CancellationTokenを使用 AbortControllerを使用(比較的新しい機能)
例外処理 非同期メソッド内の例外は自動的にTaskにラップされる Promiseのrejectを使用して例外を伝播する必要がある
非同期ストリーム IAsyncEnumerable<T>を使用 Async Iteratorsを使用(比較的新しい機能)
コンテキスト管理 ConfigureAwait(false)でコンテキスト切り替えを制御 コンテキスト切り替えの概念がない(シングルスレッドモデルのため)
非同期初期化 コンストラクタで非同期初期化を直接行えない クラスのコンストラクタで直接awaitは使用できないが、即時実行非同期関数で回避可能

Web Workers を使用したマルチスレッド処理

JavaScript では、Web Workers を使用することで、メインスレッドとは別のバックグラウンドスレッドで JavaScript を実行できます。これにより、C#のマルチスレッド処理に近い並列処理が可能になります。

main.js

// worker.jsをロード
const worker = new Worker('worker.js');

// メッセージをWorkerに送信
worker.postMessage({data: 'some data'});

// Workerからメッセージを受け取る
worker.onmessage = function(event) {
  console.log('結果: ', event.data);
};

// エラーハンドリング
worker.onerror = function(error) {
  console.error('エラーが発生しました:', error.message);
};

worker.js

self.onmessage = function(e) {
  // 重い処理を実行
  const result = heavyComputation(e.data);
  self.postMessage(result);
};

ただし、Web Workers 使用時には以下の点に注意が必要です:

  1. メインスレッドとWorker間のデータ転送にはコピーコストがかかります。
  2. 大量のデータを扱う場合は、SharedArrayBufferArrayBufferの転送(委譲)を使用してコピーコストを抑えることが重要です。
// SharedArrayBufferの使用例
const sharedBuffer = new SharedArrayBuffer(1024);
worker.postMessage({ sharedBuffer });

// ArrayBufferの転送例
const arrayBuffer = new ArrayBuffer(1024);
worker.postMessage({ arrayBuffer }, [arrayBuffer]);

C#開発者にとって、これはマルチスレッドプログラミングの経験を活かせる部分ですが、JavaScript特有の制約や最適化テクニックを理解する必要があります。

Promise の then チェーンと await の違い

JavaScript では、Promise を扱う方法として then チェーンと await の2つがあります。以下に両者の違いを示します:

特性 then チェーン await
構文の簡潔さ 複雑な処理でネストが深くなる可能性あり 同期コードに近い書き方で簡潔
エラーハンドリング .catch() メソッドを使用 通常の try-catch 構文を使用
制御フロー 複雑な制御フローの表現が難しい 通常の制御構造(if文、ループなど)を使用可能
デバッグ 複雑なフローのデバッグが難しい場合がある 同期コードに近いためデバッグが比較的容易
パフォーマンス 理論上は若干有利 追加のPromiseラッピングによる微小なオーバーヘッドの可能性
並列処理 Promise.all() などで直接実装可能 Promise.all() と組み合わせて使用

then チェーンの例:

fetchData()
  .then(data => processData(data))
  .then(result => saveResult(result))
  .catch(error => handleError(error));

await の例:

async function handleData() {
  try {
    const data = await fetchData();
    const result = await processData(data);
    await saveResult(result);
  } catch (error) {
    handleError(error);
  }
}

await を使用すると、コードがより読みやすく、同期コードに近い書き方ができます。一方で、then チェーンは並列処理を表現する際に有用です。

補足情報

  1. JavaScript のメインスレッドはシングルスレッドですが、Web Workers を使用することで並列処理が可能です。ただし、DOM操作などはメインスレッドでのみ可能です。

  2. Web Workers を使用する際は、スレッド間通信のオーバーヘッドを考慮する必要があります。大量のデータを扱う場合は、SharedArrayBufferArrayBufferの転送を検討してください。

  3. C# の Task.Run() に相当する機能として、Web Workers を使用できますが、使用方法と制約が異なります。

  4. JavaScript のイベントループモデルは、C# の非同期プログラミングモデルとは根本的に異なります。長時間実行される処理は Web Worker に移動するなどして、メインスレッドのブロックを避けることが重要です。

これらの違いを理解することで、C# 開発者は JavaScript の非同期プログラミングとマルチスレッド処理をより効果的に習得し、両言語の強みを活かした開発が可能になります。

参考情報

Discussion

junerjuner

最初の js の function をその前にある c# の方と揃えて async function にして Promiseawait するか、 c# の方を async 外して ContinueWithするようにしたほうが良かったのでは

amay077amay077

juner さんと同じような指摘かもですが、最初の「C# の Task の例:」が ”Task を使う例” であるのに対して、「JavaScript の Promise の例:」は ”Promise を作る(作って使う)例” なのが非対称なのかなと思います。

下のような感じなら Promise を使う例 になると思います。

// Task.Delay を模した関数
function delay(time) {
  return new Promise(resolve => setTimeout(resolve, time));
}

async function fetchData() {
  await delay(2000);
  return "Data fetched";
}
Atsushi NakamuraAtsushi Nakamura

juner さんの前者方式に統一しつつ、内容もできるだけ等価にしてみたつもりですが、こんな感じですかね?

junerjuner

javascript の メインスレッドはシングルスレッドではありますが、並行処理は Worker を立てることでマルチスレッド処理もできたりします(ただし、postMessage のコピーコストとか問題があるので SharedArrayBuffer 使うとか ArrayBuffer を委譲するとかして コピーコスト抑えるのが肝心だったりします。

https://developer.mozilla.org/ja/docs/Web/API/Web_Workers_API/Transferable_objects

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer

Atsushi NakamuraAtsushi Nakamura

ありがとうございます!
教えていただいた内容を読んでみて、理解の上、記事に反映したいと思います!
あざます!