thumbnail

【TypeScript】allとraceだけじゃない!4つのPromise並列処理の違い【JavaScript】

TypeScript/ JavaScriptのPromiseの並列処理メソッド『all, race, allSetteled, any』について、実際のコードと図を用いて分かりやすく解説します。

Promiseの並列処理基礎

all, race, allSetteled, anyとは

Promise型をawaitするコードは、非同期処理を1つずつ待ってから順番に実行する『直列型』の処理です。

それに対して『all, race, allSetteled, any』は非同期処理を同時に、複数実行するためのメソッドです。

公式ドキュメントには『Promiseの並列処理メソッド』と説明されています。ですので、この記事でも『並列処理』のメソッドとして説明します。allやraceは前からありますが、allSetteledやanyは割と最近追加されました。

シングルスレッド上の処理なので厳密に言うと並列処理ではありませんが、小難しいことは最初は気にしなくてかまいません。気になる方は『JavaScript 並列処理 マルチスレッド』などで検索してみてください。

この記事を読むにあたり、基本的なPromise型やasync, awaitの使い方やresolve, rejectについてご存じのない方は次の記事を参考にしてください。

all, race, allSetteled, anyの大別

『all, race, allSetteled, any』は次の2種類に分けられます。

  • ①同時に実行する非同期処理の全ての結果が欲しい場合: 『all/ allSettled』
  • ②同時に実行する非同期処理のうち1番早く終わった結果だけ欲しい場合:『any/ race』

例えばA, B, Cの非同期処理を同時に走らせ、終わる順番がC→A→Bだとします。
①のメソッド(all/ allSettled)は終わる順番に関わらずA, B, Cすべての結果を取得しますが、②のメソッド(any/ race)は一番早く終わるCの結果しか取得しません。

また、①, ②にそれぞれ2つずつメソッドがありますが、その違いは『エラー発生時』の挙動の違いにあります。このことに関しては以下で詳しく解説していきます。

基本的な文法

どのメソッドも、同時に実行したい非同期処理(Promise型)を配列にして引数に渡します。ここでは非同期処理を3つ並列に実行することを考えます。

Promise.all([非同期処理1, 非同期処理2, 非同期処理3])
Promise.allSettled([非同期処理1, 非同期処理2, 非同期処理3])
Promise.any([非同期処理1, 非同期処理2, 非同期処理3])
Promise.race([非同期処理1, 非同期処理2, 非同期処理3])

all, allSetteldの場合は非同期処理1,2,3の『全ての結果』が欲しいので、各結果に対応する要素を配列のデストラクチャリングで用意する必要があります。

const [result1, result2, result3] 
  = await Promise.all([非同期処理1, 非同期処理2, 非同期処理3])
const [result1, result2, result3] 
  = await Promise.allSettled([非同期処理1, 非同期処理2, 非同期処理3])

どのメソッドもPromise型を返すので、await句をつける必要があります。

一方、any, raceの場合は非同期処理のうち『1つの結果』のみが欲しいので、配列ではなく単独の値を用意します。

const result = await Promise.any([非同期処理1, 非同期処理2, 非同期処理3])
const result = await Promise.race([非同期処理1, 非同期処理2, 非同期処理3])

基本的な文法が分かったところで、それぞれの使い方と意味について順番に解説していきます。

4つの並列処理の使い方と意味

解説にあたって

非同期処理の具体的なコードとして、次節の解説からsetTimeoutを用いたPromiseを扱います。Promiseがresolveするのかrejectするのか区別しやすくするため、次のように、『setTimeoutで待つ秒数』と、『resolveするかrejectするかを選択するモード』を引数で渡せる関数を作成して、それを例に説明していきます。

例えば、3秒でresolveする関数,2秒でrejectする関数は次のように使えます。

promise(3, "resolve") // 3秒でresolve
promise(2, "reject") // 2秒でreject

この関数の具体的な実装は次の通りです。

/**
 * setTimeoutでresolveかrejectを選択できる関数
 * @param sec setTimeoutで待つ秒数
 * @param mode resolveするかrejectするかを文字列リテラルで指定
 * @returns Promise型の値
 */
const promise = (sec: number, mode: 'resolve' | 'reject') 
  => new Promise<string>((resolve, reject) => {
  setTimeout(() => {
      mode === 'resolve' ? resolve(`resolved: ${sec}`) : reject(`Rejected: ${sec}`)
  }, sec * 1000)
})

次のようにtry…catch分を使って、rejectされた場合にもきちんとメッセージが表示されるようにします。

;(async () => {
  try {
    const result1 = await promise(3, 'resolve')
    console.log(result1)
    const result2 = await promise(2, 'reject')
  } catch (err) {
    console.error(err)
  }
})()
[LOG]: "resolved: 3" 
[ERR]: "Rejected: 2" 

それでは実際に並列処理を行うメソッドについて見ていきます。

all

allは、同時に実行する複数の非同期処理の『全ての結果』が欲しい場合に利用しますが、複数の非同期処理のうち1つでもrejectされると全体がrejectになりエラーとなります。以下の2つのケースが考えられます。

ケース1:全部resolveされる場合

同時に実行した非同期処理が、それぞれきちんとresolveされて値を返します。

const all = async () => {
    try {
        const [p1, p2, p3] = await Promise.all([
            promise(2, 'resolve'), //A
            promise(1, 'resolve'), //B
            promise(5, 'resolve') //C
        ])
        console.log(p1, p2, p3)
    } catch (err) {
       console.error(err)
    } 
}

all()

出力

[LOG]: "resolved: 2",  "resolved: 1",  "resolved: 5" 

値が返されるタイミングは、最も時間がかかった非同期処理がresolveされた時点です。上のコードのpromise最も時間がかかるのはCの5秒なので、このallが終わるのは5秒後です。

ケース2:どれか1つがrejectされる場合

allは複数の処理のうち、どれか1つがrejectされた瞬間に全体がrejectされエラーとなります。

先ほどのコードで、2秒待つ非同期処理(Aのpromise)をrejectに変えてみます。

const all = async () => {
    try {
        const [p1, p2, p3] = await Promise.all([
            promise(2, 'reject'), //A
            promise(1, 'resolve'), //B
            promise(5, 'resolve') //C
        ])
        console.log(p1, p2, p3)
    } catch (err) {
       console.error(err)
    } 
}

all()

出力

[ERR]: "Rejected: 2"

この処理結果を時系列順に考えます。実行から1秒後にBのpromiseがresolveされます。しかし2秒後にAのpromiseがrejectされ、その瞬間にエラーとなりcatch文に移行します。5秒後に実行予定だったCのpromiseは実行されません。

allSettled

allSettledはallと似てますが、途中でrejectされても、最後まで非同期処理を実行します。その代わり、返される各値は、『成功したか、失敗したか』のステータス情報が付与された、PromiseSettledResult型の値になります。

実際のコードと実行結果を見るのが早いでしょう。

const allSettled = async () => {
    try {
        const [p1, p2, p3] = await Promise.allSettled([
            promise(2, 'reject'),
            promise(1, 'resolve'),
            promise(5, 'resolve')
        ])
        console.log(p1, p2, p3)
    } catch (err) {
       console.error("error")
    } 
}

allSettled()

結果

[LOG]: {
  "status": "rejected",
  "reason": "Rejected: 2"
},  {
  "status": "fulfilled",
  "value": "resolved: 1"
},  {
  "status": "fulfilled",
  "value": "resolved: 5"
} 

このように、各logにはstatusプロパティで『resolveかrejectか』の情報が付与され、resolveの場合は『value』プロパティにresolveの結果が、rejectの場合は『reason』にエラーメッセージが表示されます。

すべてが最後まで実行されるのでこのコードの実行時間は5秒で、エラーが発生しないのでtry…catch文で囲む必要もありません。

race

raceは、非同期処理のうち、1番最初にresolveかrejectされる値を1つ返します。つまりresolveかrejectかに関係なく、最も早く終わった結果のみを返します。

const race = async () => {
    try {
        const p = await Promise.race([
            promise(1, 'reject'), //A
            promise(2, 'resolve'), //B
            promise(5, 'resolve') //C
        ])
        console.log(p)
    } catch (err) {
       console.error(err)
    } 
}

race()

結果

[ERR]: "Rejected: 1"

この例では最も早く終わるのはAのpromiseなので、Aのpromiseがresolveならresolve、例のようにrejectならばエラーを返します。

any

最も早くresolveしたpromiseを返します。raceと似てますが、anyはrejectされたpromiseは返しません。
次の2つのケースが考えられます。

ケース1:少なくとも1つがresolveする

少なくとも1つresolveする場合、最も早くresolveしたpromiseを返します。

const any = async () => {
    try {
        const p = await Promise.any([
            promise(1, 'reject'), //A
            promise(2, 'resolve'), //B
            promise(5, 'reject') //C
        ])
        console.log(p)
    } catch (err) {
       console.error(err)
    } 
}
any()

結果

[LOG]: "resolved: 2" 

Aのpromiseが最も早く終わりますがrejectなので無視し、2番目に終わるBのpromiseがresolveなので、これが採用されます。結果としてこの処理は2秒で終わります。

ケース2:全部resolveされない

すべてresolveされない場合はエラーとなり、エラーメッセージ『All promises were rejected』が表示されます。

const any = async () => {
    try {
        const p = await Promise.any([
            promise(1, 'reject'),
            promise(2, 'reject'),
            promise(5, 'reject')
        ])
        console.log(p)
    } catch (err) {
       console.error(err)
    } 
}
any()

結果

[ERR]: All promises were rejected 

anyは最初にresolveされるまで実行されるので、どれもresolveされないということは全ての非同期処理を待つことになります。結果としてこの処理は5秒かかります。

参考