Awaitを使った非同期コードの同期処理

Gobot

牛肉・豚肉・鶏肉・ジビエ情報:Awaitを使った非同期コードの同期処理

非同期処理の概念とAwait

非同期処理とは、ある処理の完了を待たずに次の処理に進むことができるプログラミングの仕組みです。これにより、ユーザーインターフェースの応答性を保ったり、複数のタスクを効率的に実行したりすることが可能になります。例えば、ネットワークからのデータ取得やファイルI/Oといった時間のかかる処理を非同期で行うことで、プログラム全体がフリーズするのを防ぎます。

JavaScriptにおける非同期処理の代表的なものにPromiseがあります。Promiseは、非同期処理の最終的な完了(または失敗)とその結果を表すオブジェクトです。

そして、async/awaitは、このPromiseベースの非同期処理を、より直感的で同期的なコードのように記述できる構文糖です。asyncキーワードは、関数が非同期関数であることを示し、その関数内ではawaitキーワードを使用できるようになります。awaitキーワードは、Promiseが解決されるまで(つまり、非同期処理が完了するまで)関数の実行を一時停止します。Promiseが解決されると、awaitはPromiseの解決値を返します。

Awaitを使った非同期コードの同期処理

async/awaitを理解する上で重要なのは、「awaitは非同期処理を同期的に見せる」ということです。しかし、これはあくまでコードの見かけ上の話であり、実行されるスレッドがブロックされるわけではありません。JavaScriptはシングルスレッドで動作するため、awaitで一時停止している間も、イベントループは他のタスクを実行することができます。

例えば、牛肉、豚肉、鶏肉、ジビエといった複数の食材の情報を、それぞれ非同期で取得する処理を考えてみましょう。

非同期処理の例 (架空API)

まず、各食材の情報を取得する非同期関数を定義します。これらの関数は、一定時間後に食材の情報を解決するPromiseを返すと仮定します。

function getBeefInfo() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve({ type: 'beef', name: '和牛', price: 5000 });
    }, 1000); // 1秒後に完了
  });
}

function getPorkInfo() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve({ type: 'pork', name: '三元豚', price: 2000 });
    }, 1500); // 1.5秒後に完了
  });
}

function getChickenInfo() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve({ type: 'chicken', name: '地鶏', price: 1500 });
    }, 800); // 0.8秒後に完了
  });
}

function getGibierInfo() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve({ type: 'gibier', name: '鹿肉', price: 3000 });
    }, 2000); // 2秒後に完了
  });
}

Awaitによる順次実行

これらの非同期関数をasync/awaitを使って順次実行する場合、コードは非常に読みやすくなります。

async function displayMeatInfoSequentially() {
  console.log("食材情報を順次取得中...");

  // 牛肉情報の取得と完了を待つ
  const beef = await getBeefInfo();
  console.log("牛肉情報:", beef);

  // 豚肉情報の取得と完了を待つ
  const pork = await getPorkInfo();
  console.log("豚肉情報:", pork);

  // 鶏肉情報の取得と完了を待つ
  const chicken = await getChickenInfo();
  console.log("鶏肉情報:", chicken);

  // ジビエ情報の取得と完了を待つ
  const gibier = await getGibierInfo();
  console.log("ジビエ情報:", gibier);

  console.log("全ての食材情報の取得が完了しました。");
}

displayMeatInfoSequentially();

このコードでは、await getBeefInfo();が実行されると、getBeefInfoのPromiseが解決されるまで、displayMeatInfoSequentially関数の実行は一時停止します。Promiseが解決されると、その結果(牛肉の情報)がbeef変数に代入され、次の行のconsole.logが実行されます。その後、同様に豚肉、鶏肉、ジビエの情報取得が順次行われます。

この順次実行の合計時間は、各非同期処理の所要時間の合計に近くなります(上記の例では、1000 + 1500 + 800 + 2000 = 5300ミリ秒、つまり5.3秒程度)。

Awaitによる並列実行

一方で、各食材情報の取得は互いに独立しているため、同時に開始して並列に処理することで、全体の実行時間を短縮できます。awaitは順次実行を促しますが、Promise.allと組み合わせることで、複数の非同期処理を並列に実行し、その全てが完了するのをawaitで待つことができます。

async function displayMeatInfoConcurrently() {
  console.log("食材情報を並列取得中...");

  const [beef, pork, chicken, gibier] = await Promise.all([
    getBeefInfo(),
    getPorkInfo(),
    getChickenInfo(),
    getGibierInfo()
  ]);

  console.log("牛肉情報:", beef);
  console.log("豚肉情報:", pork);
  console.log("鶏肉情報:", chicken);
  console.log("ジビエ情報:", gibier);

  console.log("全ての食材情報の並列取得が完了しました。");
}

displayMeatInfoConcurrently();

このコードでは、Promise.allに渡された全てのPromiseが同時に開始されます。await Promise.all([...])は、渡された全てのPromiseが解決されるまで関数の実行を一時停止します。全てのPromiseが解決されると、Promise.allは解決された値の配列を返します。この配列を分割代入(const [beef, pork, chicken, gibier] = ...)することで、各食材の情報を取得できます。

この並列実行の合計時間は、最も時間のかかる非同期処理の所要時間に近くなります(上記の例では、最も時間のかかるジビエ情報の取得に2000ミリ秒かかるため、約2秒程度で完了します)。

エラーハンドリング

非同期処理においてエラーハンドリングは非常に重要です。async/awaitでは、同期処理と同様にtry...catchブロックを使用してエラーを捕捉できます。

async function getMeatInfoWithErrorHandling() {
  try {
    console.log("エラーハンドリング付きで食材情報を取得中...");
    const beef = await getBeefInfo();
    console.log("牛肉情報:", beef);

    // ここで意図的にエラーを発生させる
    throw new Error("豚肉情報の取得中にエラーが発生しました。");

    const pork = await getPorkInfo(); // この行は実行されない
    console.log("豚肉情報:", pork);

  } catch (error) {
    console.error("エラーが発生しました:", error.message);
    // エラー発生時の代替処理などを記述
  } finally {
    console.log("エラーハンドリング処理を終了します。");
  }
}

getMeatInfoWithErrorHandling();

上記の例では、getPorkInfo()の代わりに、意図的にthrow new Error(...)でエラーを発生させています。tryブロック内でエラーが発生すると、実行はその時点で中断され、catchブロックに処理が移ります。catchブロックでは、発生したエラーオブジェクトを受け取り、エラーメッセージなどを表示したり、適切な対応を行ったりできます。finallyブロックは、エラーの有無にかかわらず、必ず実行される処理を記述するのに使われます。

Promise.allを使用している場合、渡されたPromiseのうち1つでもrejectされると、Promise.all自体がrejectされ、catchブロックで捕捉されます。どのPromiseでエラーが発生したかを特定するには、追加の工夫が必要になる場合があります。

まとめ

async/awaitは、JavaScriptにおける非同期処理を、より簡潔で読みやすい同期的なコードスタイルで記述するための強力な構文です。これにより、複雑な非同期処理のフローを理解しやすくなり、開発効率が向上します。

awaitは、Promiseが解決されるまで関数の実行を一時停止させ、非同期処理を同期的に見せる役割を果たします。単独で使うと処理は順次実行されますが、Promise.allと組み合わせることで、複数の非同期処理を効率的に並列実行することが可能です。

また、try...catch構文を用いることで、非同期処理におけるエラーハンドリングも同期処理と同様に直感的に行うことができます。

牛肉、豚肉、鶏肉、ジビエといった異なる食材の情報を取得するような、複数の独立した非同期タスクがある場合、Promise.allasync/awaitの組み合わせは、全体の実行時間を最適化しつつ、コードの可読性を維持するための非常に効果的な手段となります。

これらの概念を理解し、適切に活用することで、より堅牢で効率的なJavaScriptアプリケーションを構築することができるでしょう。