0
1

More than 1 year has passed since last update.

非同期関数のループ処理 - Part1(forEach + async/await)

Last updated at Posted at 2021-09-20

処理方法とその特徴

Array.prototype.forEach()のコールバック関数に非同期の関数であるデータベースクエリなどを入れてループ処理を行った際の挙動が気になったため、いくつかのパターン別にサンプルコードを作成して確認してみました。今回は、Part1として、forEach + async/awaitから見ていきます。

その特徴ですが、非同期関数のループ処理においてforEach + async/awaitを使用すると、awaitが効かず、forEachが爆走します。

ループ処理の内容

フェイク非同期関数(fakeDatabaseQuery)を作成し、forEachのコールバックとしてフェイク非同期関数を100回実行するとどのような挙動になるのか、試してみました。

  1. 関数processArrayの中にあるforEachのループ処理によって、関数fakeDatabaseQuery()が連続して実行されますが、forEachの場合はasync/awaitによるサスペンドが効かないため、promiseオブジェクトが連続的に作成されるものの、それぞれのpromiseはresolveされた順番で次のプロセスに渡されます。

  2. 関数fakeDatabaseQuery()の中でpromiseオブジェクトを生成しますが、setTimeoutは、ランダムなelapseの値を使って、resolveする際の遅延を作り出しています。そのため、連続してpromiseが生成されるのですが、elapseの値がランダムであるため、resolveされる順番がバラバラとなります。

  3. awaitは常にpromiseオブジェクトを待ち受ける役割となります。今回は、awaitによるfakeDatabaseQuery()のサスペンドとレジュームは効きませんが、promiseがresolveされ次第(=>この状態をfulfilledといいます)、awaitはpromiseから値を取り出して、変数objにその値を代入します。

  4. awaitによってpromiseオブジェクトから取り出された値は、次のプロセスとして、fakeSearchResult.push()に渡され、その渡された順番でアレイfakeSearchResultに詰め込まれていきます。

  5. ただし、その後ソートしているので、最終的には、当初のループ処理の順番とおりになっています。

まとめ

プログラムを見ると、一見、async/awaitによりarrayのアイテムをひとつずつ順番に処理を行うように見えますが、実際にはそうはなりません。async/awaitによるサスペンドが全く効かず、暴走しているように感じますが、処理は速いです。フェイク非同期関数を実行する際、サスペンドなしで、イベントループにひたすら順番に投入していくイメージかと思います。

ちなみに、MDNのドキュメント(Array.prototype.forEach())には、

Note: forEach expects a synchronous function.

forEach does not wait for promises. Make sure you are aware of the implications while using promises (or async functions) as forEach callback.

と書いてあるのですが、たしかに、その通りになりました。

その他

下記のサンプルコードは、Node.jsv14.15.5でテストしたものです。
また、私の環境(Ubuntu + Node.js)で実行したところ、処理時間は120ms~125ms程度でした。さらに、蛇足ですが、arrayのアイテム数を100=>10000(百倍)などにしても、処理時間はそれほど変わらず、120ms=>190msでした。

サンプルコード

console.time();

let fakeSearchResult = [];

//forLoopを使って、整数1~100が入っているarrayを作ります
let arrayN = [];
const arrayLength = 100;
for (let i = 0; i < arrayLength; i++) {
    arrayN[i] = i + 1;
}

//最後にソートをかけて、元の順番に戻すための関数
function arraySorter(array){
    array.sort(function(a, b) {
        return a.index - b.index;
    });
}

//データベース検索を偽装したフェイク非同期関数
function fakeDatabaseQuery(item) {
    //ランダム関数で2桁の整数を作成します
    const elapse = Math.floor(Math.random() * 100);
    //プロミスオブジェクトをリターンします
    return new Promise(function(resolve, reject) {
        const obj = {index:item, elapse:elapse}
        //setTimeoutを使い、意図的に遅延を作りだしています
        setTimeout(() => resolve(obj), elapse);
    });
}

//forEachを使って、フェイク非同期関数をループ実行
function processArray(array) {
    array.forEach(async (item) => {
        //awaitはプロミスオブジェクトを待っていて、fulfilledになると、
        //プロミスオブジェクトから値を取り出します
        const obj = await fakeDatabaseQuery(item);
        fakeSearchResult.push(obj);
        if (fakeSearchResult.length === array.length){
            console.log('fakeSearchResult-1:',fakeSearchResult);//順番がぐだぐだです
            arraySorter(fakeSearchResult);//arrayにソートをかけて、当初の順番に戻します
            console.log('fakeSearchResult-2:',fakeSearchResult);//ソートして元の順番に戻りました
            console.timeEnd();
        }
    });
}

processArray(arrayN);

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1