TypeScriptの非同期イテレーター「for await…of」の使い方と応用

TypeScriptの非同期処理は、現代のJavaScriptアプリケーション開発において非常に重要な役割を果たします。非同期イテレーターは、非同期なデータストリームを効率的に処理するための強力なツールであり、特にfor await...of構文を使用することで、逐次データを簡潔に処理できます。本記事では、TypeScriptで非同期イテレーターを使うための基本的な概念から、具体的な実装例や応用方法までを詳しく解説し、非同期処理のパフォーマンスを最大限に引き出す方法についても取り上げます。

目次
  1. 非同期イテレーターの概要
  2. 「for await…of」構文とは
  3. 非同期イテレーターと同期イテレーターの違い
    1. 同期イテレーターの動作
    2. 非同期イテレーターの動作
    3. 利点と適用範囲の違い
  4. 実践例: APIデータを非同期に処理する方法
    1. 複数のAPIリクエストを非同期に処理する
    2. コード解説
    3. 非同期イテレーターの利点
  5. 非同期ジェネレーター関数の使い方
    1. 非同期ジェネレーター関数の基本構文
    2. APIリクエストを非同期ジェネレーターで実装
    3. コード解説
    4. 非同期ジェネレーターの活用シナリオ
  6. エラーハンドリングと例外処理
    1. 基本的なエラーハンドリング
    2. コード解説
    3. 非同期イテレーターのエラーハンドリングのパターン
    4. 非同期イテレーターのエラーハンドリングのベストプラクティス
  7. パフォーマンス最適化のヒント
    1. 1. 並列処理と逐次処理の使い分け
    2. 2. チャンク処理で負荷分散
    3. 3. キャッシングによるリクエストの最適化
    4. 4. レイテンシの低減
    5. 5. レートリミットの考慮
  8. 応用例: 大量データの非同期処理
    1. 大量データをAPIから取得する
    2. コード解説
    3. ファイルのストリーム処理
    4. コード解説
    5. 大量データの並列処理
    6. まとめ
  9. 実装時の注意点
    1. 1. メモリ消費の管理
    2. 2. エラーハンドリングの徹底
    3. 3. レートリミットとリトライの考慮
    4. 4. パフォーマンスのバランス
    5. 5. 非同期処理のタイムアウト設定
    6. 6. データの順序性の保証
    7. まとめ
  10. 演習問題: 非同期イテレーターを実装してみよう
    1. 問題1: APIからページネーションでデータを取得
    2. 問題2: 非同期ファイル読み込みのジェネレーター
    3. 問題3: エラー処理を組み込んだ非同期イテレーター
    4. まとめ
  11. まとめ

非同期イテレーターの概要

非同期イテレーターは、データを順次非同期に処理するための仕組みで、通常の同期的なイテレーターとは異なり、非同期な操作を順番に待ちながら進行します。これは、ネットワークリクエストやファイル操作などの時間がかかる処理に適しており、逐次的にデータを取得・処理する場合に非常に便利です。TypeScriptでは、Symbol.asyncIteratorを用いることで、非同期イテレーターをカスタマイズして独自のデータフローを扱うことができます。

「for await…of」構文とは

for await...of構文は、非同期イテレーターを反復処理するための新しいループ構文です。通常のfor...ofが同期的なイテレーターを処理するのに対して、for await...ofはPromiseを返す非同期イテレーターを処理します。これにより、Promiseが解決されるまで待ちながら、順次データを受け取り処理することが可能です。

例えば、以下のようにAPIから非同期にデータを取得し、それを逐次処理する場合に活用できます。

async function fetchData() {
  const urls = ['https://api.example.com/data1', 'https://api.example.com/data2'];

  for await (const url of urls) {
    const response = await fetch(url);
    const data = await response.json();
    console.log(data);
  }
}

この構文を使うことで、非同期の処理フローがシンプルかつ読みやすくなり、Promiseチェーンの複雑さを軽減します。

非同期イテレーターと同期イテレーターの違い

非同期イテレーターと同期イテレーターは、データの処理方法において大きな違いがあります。同期イテレーターは、すべての要素を即座に提供できるデータセットに対して使用されますが、非同期イテレーターは、要素を非同期で取得しなければならない場合に使用されます。たとえば、同期イテレーターはメモリ内に存在する配列やセットなどのデータ構造に対して使用され、非同期イテレーターはネットワークからのデータ取得やファイルの読み込みといった時間のかかる処理に適しています。

同期イテレーターの動作

同期イテレーターは、for...of構文を使用して要素を順次処理します。例えば、以下のように配列内の要素を同期的に処理することが可能です。

const items = [1, 2, 3];

for (const item of items) {
  console.log(item); // 1, 2, 3
}

ここでは、すべてのデータがメモリ内に存在しているため、すぐに処理が行われます。

非同期イテレーターの動作

一方、非同期イテレーターは、データが逐次的に非同期で提供される場合に使われます。for await...ofを使って、非同期の処理結果を順次待ちながら処理を進めます。

async function* asyncGenerator() {
  yield Promise.resolve(1);
  yield Promise.resolve(2);
  yield Promise.resolve(3);
}

for await (const item of asyncGenerator()) {
  console.log(item); // 1, 2, 3
}

このように、非同期イテレーターではデータの取得が非同期に行われるため、次の値が得られるまで待つ必要があります。

利点と適用範囲の違い

  • 同期イテレーターはシンプルで、即時に利用可能なデータを処理するのに最適です。
  • 非同期イテレーターは、非同期のデータフローを扱う際に強力であり、ネットワークリクエストやファイルシステムなど、逐次データが非同期で提供される場面で効果を発揮します。

状況に応じて、同期と非同期のイテレーターを使い分けることが重要です。

実践例: APIデータを非同期に処理する方法

非同期イテレーターの実用的な例として、APIからデータを非同期で取得し、それを順次処理する方法を紹介します。特に、複数のAPIエンドポイントにリクエストを送り、取得したデータを非同期に処理したい場合に、for await...of構文を使うことで効率的なデータ処理が可能です。

複数のAPIリクエストを非同期に処理する

例えば、複数のAPIエンドポイントからデータを取得し、それを順次処理するシナリオを考えます。以下の例では、エンドポイントから非同期でデータを取得し、それを1つずつ処理しています。

async function fetchDataFromAPIs(apiUrls: string[]) {
  for await (const url of apiUrls) {
    try {
      const response = await fetch(url);
      const data = await response.json();
      console.log("Data from", url, data);
    } catch (error) {
      console.error("Error fetching data from", url, error);
    }
  }
}

const apiUrls = [
  'https://api.example.com/data1',
  'https://api.example.com/data2',
  'https://api.example.com/data3'
];

fetchDataFromAPIs(apiUrls);

コード解説

  1. 非同期関数fetchDataFromAPIs: APIのURLリストを引数に受け取り、for await...ofループで1つずつリクエストを送ります。
  2. 非同期リクエスト処理: await fetch(url)を使ってAPIからデータを取得し、await response.json()でそのレスポンスをJSON形式で処理します。
  3. エラーハンドリング: もしリクエストに失敗した場合は、catchブロックでエラーをログに記録します。

非同期イテレーターの利点

この方法の利点は、逐次的にAPIリクエストを行う際に、次のリクエストが完了するまで待たずに次の処理に移れる点です。これにより、非同期処理をシンプルかつ読みやすい形で実装できます。

  • パフォーマンス: 非同期処理により、APIへのリクエストを効率的に管理し、レスポンスを順次処理します。
  • エラー管理: 各APIリクエストに対して個別にエラーハンドリングが可能です。

このような実装は、例えば複数の外部APIからデータを逐次取得して表示するアプリケーションや、バッチ処理を行うスクリプトに適しています。

非同期ジェネレーター関数の使い方

非同期ジェネレーター関数は、非同期イテレーターを作成するための便利な機能で、async function*という構文を使用して定義します。通常のジェネレーター関数と同様に、yieldキーワードを使って値を返しますが、非同期ジェネレーター関数では、yieldで返される値がPromiseであることが特徴です。この機能を利用すると、非同期処理の流れを管理しながら、逐次的にデータを生成することが可能です。

非同期ジェネレーター関数の基本構文

非同期ジェネレーター関数を定義するには、async function*を使います。次の例は、簡単な非同期ジェネレーター関数の構文です。

async function* asyncGenerator() {
  yield Promise.resolve(1);
  yield Promise.resolve(2);
  yield Promise.resolve(3);
}

この関数を使用すると、for await...ofを使って非同期にデータを反復処理できます。

async function processAsyncGenerator() {
  for await (const value of asyncGenerator()) {
    console.log(value); // 1, 2, 3
  }
}

APIリクエストを非同期ジェネレーターで実装

非同期ジェネレーター関数は、APIからのデータ取得やストリーム処理のような、非同期で大量のデータを処理するシナリオに非常に適しています。次の例では、APIからのデータを順次取得し、それを非同期ジェネレーターで処理しています。

async function* fetchApiData(apiUrls: string[]) {
  for (const url of apiUrls) {
    const response = await fetch(url);
    const data = await response.json();
    yield data;
  }
}

async function processApiData() {
  const apiUrls = [
    'https://api.example.com/data1',
    'https://api.example.com/data2',
    'https://api.example.com/data3'
  ];

  for await (const data of fetchApiData(apiUrls)) {
    console.log("Fetched data:", data);
  }
}

processApiData();

コード解説

  1. async function* fetchApiData: この非同期ジェネレーター関数では、複数のAPIエンドポイントからデータを順次取得し、それをyieldで返します。awaitを使って非同期にリクエストを行い、データを処理します。
  2. for await...ofを使用した処理: processApiData関数で、非同期ジェネレーターから返される各データを非同期に処理しています。for await...ofにより、次のデータが利用可能になるまで待機しつつ処理が進行します。

非同期ジェネレーターの活用シナリオ

  • ストリーミングデータの処理: 大量のデータを少しずつ処理する際、非同期ジェネレーターは役立ちます。例えば、ログファイルやデータストリームをリアルタイムで解析する場合などに効果的です。
  • APIデータの逐次取得: 大規模なAPIリクエストを非同期で処理し、そのデータを順次反映したい場合に便利です。

非同期ジェネレーター関数を活用することで、データのストリーム処理やリアルタイムの非同期処理をシンプルに実装できるようになります。

エラーハンドリングと例外処理

非同期イテレーターを使用する際に重要なのが、エラーハンドリングと例外処理です。非同期処理では、外部のAPIリクエストやファイル読み込みなど、成功しない場合が頻繁に発生します。これに対処するため、適切なエラーハンドリングを組み込むことで、アプリケーションの安定性を保つことができます。特に、非同期ジェネレーターやfor await...of構文を使った非同期処理では、エラーが発生した際にどのように対応するかを計画しておく必要があります。

基本的なエラーハンドリング

非同期イテレーター内でエラーが発生する場合、通常のtry...catch文を使用して例外をキャッチできます。for await...of構文内でも同様に、エラーハンドリングを行うことができます。

async function* fetchApiDataWithErrors(apiUrls: string[]) {
  for (const url of apiUrls) {
    try {
      const response = await fetch(url);
      if (!response.ok) {
        throw new Error(`Failed to fetch data from ${url}`);
      }
      const data = await response.json();
      yield data;
    } catch (error) {
      console.error("Error:", error);
      yield null; // エラーが発生した場合、nullを返す
    }
  }
}

async function processApiDataWithErrors() {
  const apiUrls = [
    'https://api.example.com/data1',
    'https://api.example.com/data2',
    'https://api.invalid-url.com/data3' // 無効なURLでエラーを発生させる
  ];

  for await (const data of fetchApiDataWithErrors(apiUrls)) {
    if (data) {
      console.log("Fetched data:", data);
    } else {
      console.log("Failed to fetch data for one of the URLs");
    }
  }
}

processApiDataWithErrors();

コード解説

  1. try...catchでエラーをキャッチ: fetchApiDataWithErrors関数内では、各APIリクエストごとにtry...catchを使用して、リクエストが失敗した場合の例外をキャッチします。fetchが失敗した場合や、レスポンスが失敗を示す場合はエラーを投げます。
  2. エラー発生時の処理: エラーが発生した場合は、nullyieldすることで、そのエラーを処理側に伝えます。呼び出し元では、返ってきたデータがnullかどうかを確認し、適切な処理を行います。

非同期イテレーターのエラーハンドリングのパターン

非同期処理のエラーハンドリングにはいくつかのパターンがあります。

1. 個別処理ごとのエラーキャッチ

各イテレーションごとにtry...catchを使ってエラーを個別にキャッチし、処理を続行するパターンです。これにより、エラーが発生しても他のデータ処理に影響を与えません。

2. 全体でのエラーキャッチ

for await...ofループ全体をtry...catchで囲むことで、処理全体に影響を与えるエラーを一度にキャッチします。これにより、重大なエラーが発生した場合、すぐに処理を停止させることができます。

async function processApiData() {
  try {
    const apiUrls = [
      'https://api.example.com/data1',
      'https://api.invalid-url.com/data2'
    ];

    for await (const data of fetchApiDataWithErrors(apiUrls)) {
      console.log("Data:", data);
    }
  } catch (error) {
    console.error("Critical error:", error);
  }
}

非同期イテレーターのエラーハンドリングのベストプラクティス

  • 早期にエラーをキャッチ: 各処理ごとにエラーハンドリングを組み込むことで、エラーが他の処理に影響を与えないようにします。
  • ロギング: エラー発生時に適切にログを残すことで、問題の原因を後から確認できるようにします。
  • レスポンスのバリデーション: 非同期処理の結果が期待通りのデータであるかを常に確認し、異常値が返された場合にすぐに対処できるようにします。

非同期イテレーターでは、エラー処理が他の非同期処理と同様に重要です。正確なエラーハンドリングを行うことで、アプリケーションの安定性と信頼性を高めることができます。

パフォーマンス最適化のヒント

非同期イテレーターを使用した処理は、効率的で柔軟ですが、大量のデータを扱ったり、複数の非同期リクエストを行う際にはパフォーマンスの最適化が重要です。非同期イテレーターを使って処理を高速化し、アプリケーションの応答性を高めるためには、いくつかのテクニックがあります。

1. 並列処理と逐次処理の使い分け

非同期処理では、すべてのタスクを逐次的に処理するのではなく、可能な場合は並列処理を利用することで、処理時間を大幅に短縮できます。for await...of構文は逐次的にPromiseを解決しますが、複数の非同期処理を並行して実行したい場合は、Promiseを並列で処理するテクニックを使います。

例えば、複数のAPIリクエストを同時に処理するためには、Promise.allを使って並列化が可能です。

async function fetchParallel(apiUrls: string[]) {
  const promises = apiUrls.map(url => fetch(url).then(response => response.json()));
  const results = await Promise.all(promises);
  results.forEach(data => console.log("Data:", data));
}

この方法では、すべてのリクエストが並行して実行され、全てのPromiseが解決されるまで待機します。

2. チャンク処理で負荷分散

大量のデータを処理する際には、一度にすべてのデータを処理するのではなく、データを小さな「チャンク」に分けて処理することが効果的です。これにより、メモリ使用量を抑え、システムにかかる負荷を軽減することができます。

async function* chunkedGenerator(data: any[], chunkSize: number) {
  for (let i = 0; i < data.length; i += chunkSize) {
    yield data.slice(i, i + chunkSize);
  }
}

async function processInChunks() {
  const largeDataSet = [...Array(10000).keys()];
  const chunkSize = 1000;

  for await (const chunk of chunkedGenerator(largeDataSet, chunkSize)) {
    console.log("Processing chunk:", chunk);
    // 各チャンクを処理
  }
}

この例では、1000件ずつのデータをチャンクに分けて処理することで、メモリとパフォーマンスのバランスを取っています。

3. キャッシングによるリクエストの最適化

同じリソースへの非同期リクエストが何度も発生する場合、キャッシュを導入することでパフォーマンスを向上させることができます。リクエスト結果を一時保存し、次回同じリクエストが来たときに再利用することにより、不要なネットワークアクセスを減らします。

const cache = new Map<string, any>();

async function fetchWithCache(url: string) {
  if (cache.has(url)) {
    return cache.get(url);
  }
  const response = await fetch(url);
  const data = await response.json();
  cache.set(url, data);
  return data;
}

async function processWithCache(apiUrls: string[]) {
  for await (const url of apiUrls) {
    const data = await fetchWithCache(url);
    console.log("Data from cache or API:", data);
  }
}

キャッシングを導入することで、同一のリソースに対するリクエスト数を減らし、パフォーマンスを大幅に改善できます。

4. レイテンシの低減

ネットワークレイテンシは、非同期処理において大きなパフォーマンス低下の原因となります。これを改善するための方法として、次のようなテクニックがあります。

  • APIのバッチリクエスト: 複数のリクエストを一度に送信し、サーバー側でバッチ処理することでレイテンシを減らします。
  • CDNの利用: より近いサーバーからリソースを提供することで、ネットワークレイテンシを減少させます。

5. レートリミットの考慮

大量のAPIリクエストを送信する場合、サービスが設定しているレートリミットに引っかかる可能性があります。これを防ぐため、リクエストの送信速度を制御することが必要です。一定の待機時間を設けてリクエストを送信する「スロットリング」や「デバウンシング」といったテクニックを使います。

async function fetchWithThrottle(apiUrls: string[], delay: number) {
  for (const url of apiUrls) {
    const response = await fetch(url);
    const data = await response.json();
    console.log("Data:", data);
    await new Promise(resolve => setTimeout(resolve, delay)); // リクエスト間の待機時間
  }
}

fetchWithThrottle(['https://api.example.com/data1', 'https://api.example.com/data2'], 1000);

このように、非同期イテレーターを使用した処理でも、パフォーマンスの最適化を意識することで、システムの負荷を軽減しながら効率的な非同期処理が可能となります。

応用例: 大量データの非同期処理

大量データを扱う場合、非同期イテレーターはその効率性と柔軟性から特に役立ちます。通常、膨大なデータを一度にメモリに読み込むとパフォーマンスに悪影響を与えたり、メモリ不足を引き起こす可能性があります。非同期イテレーターを使うことで、データを一度に処理するのではなく、段階的に少しずつ取得し、必要な分だけをメモリに保持して処理できます。

大量データをAPIから取得する

例えば、サーバーから大量のデータを複数回に分けて取得し、順次処理するシナリオを考えます。このような場合、サーバー側のページネーション(ページごとのデータ取得)やチャンク化されたデータを非同期イテレーターで効率的に処理できます。

async function* fetchPaginatedData(apiUrl: string, pageSize: number) {
  let page = 1;
  let hasMoreData = true;

  while (hasMoreData) {
    const response = await fetch(`${apiUrl}?page=${page}&size=${pageSize}`);
    const data = await response.json();

    if (data.length < pageSize) {
      hasMoreData = false; // すべてのデータを取得した場合
    }

    yield data;
    page++;
  }
}

async function processLargeData(apiUrl: string) {
  const pageSize = 100;

  for await (const dataChunk of fetchPaginatedData(apiUrl, pageSize)) {
    console.log("Processing data chunk:", dataChunk);
    // 各データチャンクを順次処理
  }
}

processLargeData('https://api.example.com/large-data');

コード解説

  1. fetchPaginatedData関数: 非同期ジェネレーターとして実装されており、指定されたAPIエンドポイントからページごとにデータを取得します。ページサイズを指定して、1回のリクエストでどれだけのデータを取得するかを制御できます。すべてのデータが取得されると、hasMoreDatafalseにしてループを終了します。
  2. for await...ofでの非同期処理: processLargeData関数では、非同期イテレーターからチャンクごとにデータを受け取り、処理を行います。for await...ofによって、非同期的に次のデータチャンクが到着するまで待ちながら処理が進行します。

ファイルのストリーム処理

もう1つの実例として、大規模なファイルのストリーミング処理があります。大きなファイルをすべてメモリに読み込まず、ストリームとして非同期に少しずつ読み込むことで、メモリ効率を向上させながら処理が可能です。

import * as fs from 'fs';
import { createReadStream } from 'fs';
import { createInterface } from 'readline';

async function* readLargeFile(filePath: string) {
  const fileStream = createReadStream(filePath);
  const rl = createInterface({
    input: fileStream,
    crlfDelay: Infinity
  });

  for await (const line of rl) {
    yield line;
  }
}

async function processLargeFile(filePath: string) {
  for await (const line of readLargeFile(filePath)) {
    console.log("Processing line:", line);
    // 各行を順次処理
  }
}

processLargeFile('path/to/large/file.txt');

コード解説

  1. readLargeFile関数: 非同期ジェネレーターを使って、テキストファイルを1行ずつストリーム処理します。ファイル全体をメモリに読み込まずに処理できるため、非常に大きなファイルにも適しています。
  2. for await...ofでの行ごとの処理: processLargeFile関数では、各行を非同期に受け取り、逐次処理を行います。これにより、メモリ消費量を抑えつつ大量のデータを効率的に処理できます。

大量データの並列処理

大量データの処理には、並列化することでパフォーマンスを向上させることができます。ただし、並列処理するデータの量を適切に管理しないと、システムに過剰な負荷をかけてしまうことがあります。以下のコード例では、非同期に複数のAPIリクエストを並行して行い、データを並列処理します。

async function processLargeDataInParallel(apiUrl: string, pageSize: number, parallelLimit: number) {
  let page = 1;
  let hasMoreData = true;

  while (hasMoreData) {
    const promises = [];

    for (let i = 0; i < parallelLimit; i++) {
      promises.push(fetch(`${apiUrl}?page=${page + i}&size=${pageSize}`).then(response => response.json()));
    }

    const dataChunks = await Promise.all(promises);

    for (const dataChunk of dataChunks) {
      if (dataChunk.length < pageSize) {
        hasMoreData = false;
      }
      console.log("Processing data chunk:", dataChunk);
    }

    page += parallelLimit;
  }
}

processLargeDataInParallel('https://api.example.com/large-data', 100, 5);

まとめ

大量データの非同期処理は、パフォーマンスを最大限に引き出し、メモリ効率を高めるために非常に効果的です。非同期イテレーターを使うことで、大規模なデータを段階的に処理し、システム負荷を分散しながら効率的にデータを処理することが可能です。

実装時の注意点

非同期イテレーターを実装する際には、いくつかの注意点があります。特に、大量のデータを扱う場合や、外部APIとの非同期通信を行う場合、パフォーマンスや信頼性に影響を与える可能性があるため、これらの点に気を配ることが重要です。

1. メモリ消費の管理

非同期イテレーターを使用して大規模なデータを処理する場合、メモリ消費量の管理が非常に重要です。全てのデータを一度にメモリにロードするのではなく、必要なデータのみを段階的に取得する設計にすることが基本です。大量データのストリーミングやページネーションなどを利用して、データ処理を段階的に行うことで、メモリ使用量を抑えられます。

async function* fetchPaginatedData(apiUrl: string, pageSize: number) {
  let page = 1;
  let hasMoreData = true;

  while (hasMoreData) {
    const response = await fetch(`${apiUrl}?page=${page}&size=${pageSize}`);
    const data = await response.json();

    if (data.length < pageSize) {
      hasMoreData = false;
    }

    yield data;
    page++;
  }
}

このように、ページごとにデータを取得することで、一度に大量のデータを扱う場合でもメモリ効率を高めることができます。

2. エラーハンドリングの徹底

非同期処理では、予期しないエラーが発生することが多いため、しっかりとしたエラーハンドリングを実装することが必要です。特に、ネットワークの不安定さや、外部APIが不正なレスポンスを返した場合などに対応するため、try...catch構文を活用して、例外処理を適切に行います。

async function* fetchDataWithErrorHandling(apiUrls: string[]) {
  for (const url of apiUrls) {
    try {
      const response = await fetch(url);
      if (!response.ok) {
        throw new Error(`Failed to fetch data from ${url}`);
      }
      const data = await response.json();
      yield data;
    } catch (error) {
      console.error(`Error fetching from ${url}:`, error);
      yield null;
    }
  }
}

この例では、APIリクエストが失敗した場合にもエラーを適切にキャッチし、エラーが発生した際には代替処理を行っています。

3. レートリミットとリトライの考慮

外部APIを利用する場合、レートリミット(一定時間内のリクエスト回数制限)が設定されていることがあります。これに違反すると、リクエストが拒否されるため、適切な間隔を保つためにリトライやスロットリング(リクエスト間の待機時間を設ける)を実装することが重要です。

async function fetchWithRetry(url: string, retries: number = 3) {
  for (let attempt = 1; attempt <= retries; attempt++) {
    try {
      const response = await fetch(url);
      if (!response.ok) throw new Error("Failed request");
      return await response.json();
    } catch (error) {
      if (attempt < retries) {
        console.log(`Retrying... (${attempt}/${retries})`);
        await new Promise(resolve => setTimeout(resolve, 1000)); // 1秒待機してリトライ
      } else {
        throw error;
      }
    }
  }
}

リトライ機能を組み込むことで、リクエストが失敗しても数回再試行する仕組みを実装できます。

4. パフォーマンスのバランス

並列処理は効率的ですが、並行して実行するタスクが多すぎると、逆にシステムに負荷をかけ、処理速度が低下する可能性があります。例えば、APIリクエストの並列処理では、同時に処理するタスクの数を適切に制限する必要があります。

async function processInParallelWithLimit(urls: string[], parallelLimit: number) {
  const queue = [...urls];
  const promises = [];

  while (queue.length > 0) {
    if (promises.length < parallelLimit) {
      const url = queue.shift();
      const promise = fetch(url).then(response => response.json());
      promises.push(promise);
    }

    if (promises.length >= parallelLimit) {
      const result = await Promise.race(promises);
      console.log(result);
      promises.splice(promises.indexOf(result), 1); // 完了したタスクを削除
    }
  }

  await Promise.all(promises); // 残りの全タスクを完了させる
}

この方法では、同時に実行するタスクの数をparallelLimitで制限し、システムに過剰な負荷がかからないように制御しています。

5. 非同期処理のタイムアウト設定

非同期処理にはタイムアウト設定が欠かせません。外部APIが応答しない場合に、無限に待ち続けてしまうとアプリケーション全体のパフォーマンスが低下します。適切なタイムアウトを設定することで、処理が長時間停滞するのを防ぎます。

async function fetchWithTimeout(url: string, timeout: number = 5000) {
  const controller = new AbortController();
  const signal = controller.signal;
  const timeoutId = setTimeout(() => controller.abort(), timeout);

  try {
    const response = await fetch(url, { signal });
    clearTimeout(timeoutId);
    return await response.json();
  } catch (error) {
    if (signal.aborted) {
      console.error("Request timed out");
    } else {
      throw error;
    }
  }
}

このコードでは、指定した時間が経過するとリクエストが自動的にキャンセルされ、タイムアウト処理が行われます。

6. データの順序性の保証

非同期処理を行う際、データの順序が重要な場合があります。特に並列処理を行うと、結果が処理された順序が前後してしまうことがあります。順序を保証したい場合は、Promiseの結果を保存しておき、順番に処理することが必要です。

async function processInOrder(urls: string[]) {
  const promises = urls.map(async url => {
    const data = await fetch(url).then(response => response.json());
    return { url, data };
  });

  const results = await Promise.all(promises);
  results.sort((a, b) => urls.indexOf(a.url) - urls.indexOf(b.url)); // 元の順序に戻す

  for (const result of results) {
    console.log(result.data);
  }
}

ここでは、各Promiseの結果を元の順序に並べ替えてから処理しています。

まとめ

非同期イテレーターの実装には、パフォーマンス、エラーハンドリング、メモリ効率など多くの点で注意が必要です。これらの点に注意しながら実装することで、安定した高性能な非同期処理を行うことができます。

演習問題: 非同期イテレーターを実装してみよう

ここでは、非同期イテレーターの理解を深めるために、実際に自分で実装してみる演習問題を用意しました。以下の問題に取り組むことで、for await...of構文や非同期ジェネレーターの実践的な使い方を習得できます。

問題1: APIからページネーションでデータを取得

APIエンドポイント https://api.example.com/data?page=1&size=10 を使って、10件ずつページごとにデータを取得し、全ページのデータを順次処理する非同期ジェネレーター関数を実装してください。

  • 各ページのデータを取得するための非同期ジェネレーターを作成する。
  • for await...ofを使って、取得したデータを順次処理する。

実装ヒント

  1. fetchPaginatedDataという非同期ジェネレーター関数を作成します。
  2. 各ページのデータをAPIから取得し、次のページが存在する限りデータをyieldします。
  3. for await...ofを使って、すべてのページデータを順次処理します。
async function* fetchPaginatedData(apiUrl: string, pageSize: number) {
  let page = 1;
  let hasMoreData = true;

  while (hasMoreData) {
    const response = await fetch(`${apiUrl}?page=${page}&size=${pageSize}`);
    const data = await response.json();

    if (data.length < pageSize) {
      hasMoreData = false;
    }

    yield data;
    page++;
  }
}

async function processAllPages(apiUrl: string) {
  const pageSize = 10;

  for await (const pageData of fetchPaginatedData(apiUrl, pageSize)) {
    console.log("Processing page data:", pageData);
    // データ処理をここで実行
  }
}

processAllPages('https://api.example.com/data');

問題2: 非同期ファイル読み込みのジェネレーター

大きなテキストファイル(large-file.txt)を1行ずつ読み込み、各行のデータを順次処理する非同期ジェネレーターを作成してください。

  • ファイルストリームを使って、非同期に1行ずつファイルを読み込みます。
  • 読み込んだデータを順次処理します。

実装ヒント

  1. fsモジュールのcreateReadStreamを使って大きなファイルをストリーム形式で読み込みます。
  2. readlineモジュールを利用して1行ずつ読み込み、非同期ジェネレーターとしてyieldします。
  3. for await...ofを使って、1行ずつデータを処理します。
import * as fs from 'fs';
import { createInterface } from 'readline';

async function* readFileLineByLine(filePath: string) {
  const fileStream = fs.createReadStream(filePath);
  const rl = createInterface({
    input: fileStream,
    crlfDelay: Infinity
  });

  for await (const line of rl) {
    yield line;
  }
}

async function processFile(filePath: string) {
  for await (const line of readFileLineByLine(filePath)) {
    console.log("Processing line:", line);
    // 各行のデータをここで処理
  }
}

processFile('large-file.txt');

問題3: エラー処理を組み込んだ非同期イテレーター

APIからデータを取得する際に、エラーが発生した場合にリトライ機能を組み込んだ非同期ジェネレーターを作成してください。リクエストが3回失敗したら、そのデータの取得をスキップして次のデータへ進むようにします。

実装ヒント

  1. APIリクエスト部分にリトライ機能を実装します。
  2. リクエストが3回失敗したら、エラーログを出力し、そのリクエストをスキップします。
async function* fetchWithRetry(apiUrls: string[], retries: number = 3) {
  for (const url of apiUrls) {
    let attempt = 0;
    let success = false;

    while (attempt < retries && !success) {
      try {
        const response = await fetch(url);
        if (!response.ok) throw new Error("Request failed");
        const data = await response.json();
        yield data;
        success = true;
      } catch (error) {
        attempt++;
        if (attempt >= retries) {
          console.error(`Failed to fetch data from ${url} after ${retries} attempts`);
          yield null; // エラーが発生した場合はnullを返す
        }
      }
    }
  }
}

async function processWithRetry(apiUrls: string[]) {
  for await (const data of fetchWithRetry(apiUrls)) {
    if (data) {
      console.log("Fetched data:", data);
    } else {
      console.log("Failed to fetch data");
    }
  }
}

processWithRetry(['https://api.example.com/data1', 'https://api.example.com/data2']);

まとめ

この演習を通して、非同期イテレーターやfor await...ofを活用して効率的にデータを処理する方法を理解できたはずです。非同期ジェネレーターを使った大量データの処理やエラーハンドリングの強化など、実際のアプリケーション開発における実装力を向上させましょう。

まとめ

本記事では、TypeScriptにおける非同期イテレーターとfor await...of構文の使い方について詳しく解説しました。非同期イテレーターを使用することで、APIリクエストやファイル読み込みといった非同期のデータ処理を効率的かつ柔軟に行うことができます。また、パフォーマンス最適化、エラーハンドリング、並列処理のバランスといった実装時の注意点についても学びました。非同期ジェネレーターや演習問題を通じて、実践的な知識を身につけ、非同期処理のパフォーマンスを最大限に引き出す手法を習得できたはずです。

コメント

コメントする

目次
  1. 非同期イテレーターの概要
  2. 「for await…of」構文とは
  3. 非同期イテレーターと同期イテレーターの違い
    1. 同期イテレーターの動作
    2. 非同期イテレーターの動作
    3. 利点と適用範囲の違い
  4. 実践例: APIデータを非同期に処理する方法
    1. 複数のAPIリクエストを非同期に処理する
    2. コード解説
    3. 非同期イテレーターの利点
  5. 非同期ジェネレーター関数の使い方
    1. 非同期ジェネレーター関数の基本構文
    2. APIリクエストを非同期ジェネレーターで実装
    3. コード解説
    4. 非同期ジェネレーターの活用シナリオ
  6. エラーハンドリングと例外処理
    1. 基本的なエラーハンドリング
    2. コード解説
    3. 非同期イテレーターのエラーハンドリングのパターン
    4. 非同期イテレーターのエラーハンドリングのベストプラクティス
  7. パフォーマンス最適化のヒント
    1. 1. 並列処理と逐次処理の使い分け
    2. 2. チャンク処理で負荷分散
    3. 3. キャッシングによるリクエストの最適化
    4. 4. レイテンシの低減
    5. 5. レートリミットの考慮
  8. 応用例: 大量データの非同期処理
    1. 大量データをAPIから取得する
    2. コード解説
    3. ファイルのストリーム処理
    4. コード解説
    5. 大量データの並列処理
    6. まとめ
  9. 実装時の注意点
    1. 1. メモリ消費の管理
    2. 2. エラーハンドリングの徹底
    3. 3. レートリミットとリトライの考慮
    4. 4. パフォーマンスのバランス
    5. 5. 非同期処理のタイムアウト設定
    6. 6. データの順序性の保証
    7. まとめ
  10. 演習問題: 非同期イテレーターを実装してみよう
    1. 問題1: APIからページネーションでデータを取得
    2. 問題2: 非同期ファイル読み込みのジェネレーター
    3. 問題3: エラー処理を組み込んだ非同期イテレーター
    4. まとめ
  11. まとめ