Promise.allを使ってasync/awaitのループ処理を速くする

node.jsのPromise.allメソッドの使い方を解説します。

同期/非同期とか並列処理とか難しくてなかなか理解するのが大変ですが、使い方がわかれば便利です。

Promise.all()

Promise.all()メソッドは入力としてプロミスの集合の反復可能オブジェクトを取り、単一のPromiseを返します。

入力されたプロミスがすべて履行されたとき、その履行された値の配列で、履行されます。

意味わかりませんね、書いていて自分でも意味不明です。

ようするに「渡されたすべての非同期処理が終わったら履行されます」って意味かと。

使い方

購入した商品の情報を取得して合計金額を算出するサンプルコードです。

DBへ問い合わせて商品の情報取得(getItem)に2秒かかるとします。(今回は配列にリストを保持してsleepしています。)

// 商品リスト(DBの代わり)
const items = [
  { no: 1, name: 'あんぱん', price: 130 },
  { no: 2, name: '食パン', price: 300 },
  { no: 3, name: 'カレーパン', price: 150 },
  { no: 4, name: 'ジャム', price: 200 },
  { no: 5, name: 'バター', price: 250 },
  { no: 6, name: 'チーズ', price: 100 },
];

/**
 * スリープ処理
 * @param {number} m ミリ秒
 */ 
const sleep = (m) => {
  return new Promise( resolve => setTimeout( resolve, m ));
}

/**
 * 商品取得処理
 * @param {Number} no 商品番号
 * @returns 
 */
const getItem = async(no) => {
  await sleep(2000);  // 2秒かかる想定
  return items.find(v => v.no == no);
}

/**
 * 購入した商品の合計金額を計算する
 * @param {Array} buyList 購入商品No
 */
const priceTotal = async (buyList) => {
  let totalPrice = 0; // 合計金額
  const result = [];  // 購入リスト
  console.log(new Date().toLocaleTimeString('ja-JP', { hour12: false }) );  // 開始時間

  // START A1 - 商品を取得して合計金額を出す
  for (const itemNo of buyList) result.push(await getItem(itemNo));
  totalPrice = result.reduce((sum, item) => sum + item.price, 0);
  // END A1

  console.log(new Date().toLocaleTimeString('ja-JP', { hour12: false }) );  // 終了時間
  console.log(`合計金額:${totalPrice}円`);
}

// 実行
priceTotal([1,2,3]);  // あんぱん、食パン、カレーパンを購入

実行結果は下記になります。

node .\promise.js
16:02:51
16:02:57
合計金額:580円

あんぱん:130円、食パン:300円、カレーパン:150円なので合計は580円です。

簡単な足し算ですね、でも問題はそこじゃないです。

実行時間が6秒かかっています、1つの商品を取得するのに2秒かかるので、2×3で6秒です。

商品を取得する関数getItem()の前にawaitがあるので1つの商品情報を取得してから次の商品を取得します。

Promise.all()

priceTotal()をPromise.all()を使用した処理に書き換えます。

/**
 * 購入した商品の合計金額を計算する Promise.all
 * @param {Array} buyList 購入商品No
 */
const priceTotal = async (buyList) => {
  let totalPrice = 0; // 合計金額
  const result = [];  // 購入リスト
  console.log(new Date().toLocaleTimeString('ja-JP', { hour12: false }) );  // 開始時間

  // START A1 - 商品を取得して合計金額を出す
  for (const itemNo of buyList) result.push(getItem(itemNo));
  const items = (await Promise.all(result));
  totalPrice = items.reduce((sum, item) => sum + item.price, 0);
  // END A1

  console.log(new Date().toLocaleTimeString('ja-JP', { hour12: false }) );  // 終了時間
  console.log(`合計金額:${totalPrice}円`);
}

// 実行
priceTotal([1,2,3]);  // あんぱん、食パン、カレーパンを購入

実行結果は下記になります。

node .\promise.js
16:11:04
16:11:06
合計金額:580円

合計金額は変わらず580円です。

実行時間が2秒になっています、処理速度が上がっています。

getItem()の前のawaitを外したことにより、商品を取得する処理が並列に実行されます。

そしてawait Promise.all(result)により、全ての商品情報を取得し終わってからitemsに商品リストを返します。

要するに「商品情報の取得はいっぺんにやるけど、全部取得が終わったらリストを返して!」って感じです。

商品を1つ1つ順番に取得しないので、処理時間が短縮できました。

まとめ

今回はDBではなく配列にしてsleepで遅延を発生させました。

商品も3件と少なめです。

使い方次第ですが、大きなDBや、複雑なクエリ、大量のデータ取得などを実行するときに処理速度の向上が期待できます。

ただし、並列処理なので一時的にCPU使用率やメモリ使用率は上がると思います。

実行サーバのリソースやDB負荷なども考慮が必要になるかもしれません。

関連記事
最新記事