Javascriptで同時に非同期処理を行う
はじめに
JSでは非同期処理を使用することでHTTPリクエストを送った際にレスポンスが帰ってくるまでの間に別のリクエストを送ることができます。 なのでタイトルには同時と書いてありますが、厳密には同時ではありません。
ただ、こうすることで複数のリクエストをすばやく処理することが可能です。 しかし、サーバーに負荷をかけたくないなどの理由から同時にリクエストを送る数を制限する必要があると思います。 今回はそんな場合にリクエストをバッチ化して処理する方法についても整理してみます。
準備
今回はwait
関数を作成し、一定時間待機させるような処理を利用して、HTTPリクエストなどの代わりとします。
今回は2秒間処理をブロックするような関数を作成しました。
const sleep = (time) => new Promise(resolve => setTimeout(resolve, time)) const wait = async (i) => { console.log(`Start wait: ${i}`) await sleep(2000) console.log(`End wait: ${i}`) return i }
同期的な処理
まずはじめに同期的に処理する場合を考えます。 全部で6回分処理を行います。
const lo = require('lodash') const main = async () => { const idx = lo.range(0, 6) const start = new Date() const values = [] for (const i of idx) { const ret = await wait(i) values.push(ret) } const end = new Date() const elapsed = (end - start) / 1000 console.log(`${values}, elapsed: ${elapsed} sec`) } main()
実行結果は以下のようになります。 一つ一つの処理の完了を待ってから次の処理が行われるので処理数x2秒くらいの時間がかかります。 実際結果を見ると、12秒ほどの時間がかかっています。
Start wait: 0 End wait: 0 Start wait: 1 End wait: 1 Start wait: 2 End wait: 2 Start wait: 3 End wait: 3 Start wait: 4 End wait: 4 Start wait: 5 End wait: 5 0,1,2,3,4,5, elapsed: 12.02 sec
非同期的な処理
次は非同期的に複数の処理を行い実行時間を削減してみます。
const main = async () => { const idx = lo.range(0, 6) const start = new Date() const values = await Promise.all(idx.map(wait)) const end = new Date() const elapsed = (end - start) / 1000 console.log(`${values}, dudation: ${elapsed} sec`) } main()
sleep
の待機中に別の処理が動いていることが確認できます。
一度にすべてスタートしてから、その後逐次処理が完了しています。
ほぼ同時に処理するため処理時間は全体で2秒程度になります。
Start wait: 0 Start wait: 1 Start wait: 2 Start wait: 3 Start wait: 4 Start wait: 5 End wait: 0 End wait: 1 End wait: 2 End wait: 3 End wait: 4 End wait: 5 0,1,2,3,4,5, dudation: 2.01 sec
ここでのポイントは以下の部分です。
const values = await Promise.all(idx.map(wait))
2つのパートに分けて解説します。
1つ目は以下の部分です。
idx.map(wait)
async
関数の返り値はPromise
になります。
idx
は0〜5が含まれているArray
なので、これをmap
で変換するとPromise
のリストになります。
2つ目は以下の部分です。
Promise.all([Promise...])
Promise.all
はArray
などのiterableなPromise
引数としてとり、単一のPromiseとして処理します。
つまりはここではArray
に含まれるすべてのPromise
の処理が終わった場合に、これ以降の処理がすすみます。
なので、すべての処理が終わるまでここで待機することになります。
同時に処理する数を制御する
さて、ここまでで同期的に逐一処理する方法と、同時に処理する方法を述べてきました。 同時に処理する数を制御にはこの2つを組み合わせれば可能です。
今回は2個ずつ同時に処理し、それを3回繰り返します。
const main = async () => { const idx = lo.range(0, 6) const totalStart = new Date() for (const chunked of lo.chunk(idx, 2)) { const start = new Date() const values = await Promise.all(chunked.map(wait)) const end = new Date() const elapsed = (end - start) / 1000 console.log(`${values}, elapsed: ${elapsed} sec`) } const totalEnd = new Date() const totalElapsed = (totalEnd - totalStart) / 1000 console.log(`Total elapsed: ${totalElapsed} sec`) } main()
実行結果を確認すると2個ずつ処理が進んでいることが確認できます。 全体の処理時間は各バッチで2秒、それが3回なので計 2x3 = 6秒かかります。
Start wait: 0 Start wait: 1 End wait: 0 End wait: 1 0,1, elapsed: 2.008 sec Start wait: 2 Start wait: 3 End wait: 2 End wait: 3 2,3, elapsed: 2.004 sec Start wait: 4 Start wait: 5 End wait: 4 End wait: 5 4,5, elapsed: 2.003 sec Total elapsed: 6.016 sec
順に解説していきます。
最初は以下の部分です。
for (const chunked of lo.chunk(idx, 2)) { ... }
ここはlodash
の関数を利用してArray
を2個ずつに区切っています。
つまりlo.chunk(idx, 2)==[[0, 1], [2, 3], [4, 5]]
です。
つぎは以下の部分です。
const values = await Promise.all(chunked.map(wait))
ここでは2個区切りになったArray
を非同期的に処理しています。
つまりは2個分の処理が解決されるまでここで待機することになります。
解決されると次の2個が処理されます。
おわりに
非同期的に処理を行うことで全体の処理時間を短縮することができました。 また、同時実行数を制御することでHTTPリクエストなどを使用する際にサーバーに過度な負荷をかけることを防ぐこともできました。