非同期処理の並列実行

7月12日金曜日、小雨

霧雨のようなものだったので、帽子だけかぶって(メガネをガードし)傘は持たずに出社。気にならないくらいの降りだったけれど、でも会社につくまでのラスト15分でシャツが透けて見えるくらいには濡れる。

* * *

昨日の続き。

呼び出してから戻るまでにちょっと時間がかかる API があったとして、そういうのを Kotlin では suspend 関数にとじこめられる。例えばこんな感じだ。(本体は省略)

suspend fun resolveName(id: Long): String { ... }

昨日の話は、手元に id のリストがあったとして、その id それぞれについて名前を取ってこようとしたものだと言える。

val ids: List<Long> = ...
val names = ids.map { id -> resolveName(id) }

そして昨日書いたとおりだけれど、上述のコードは ids の中の id それぞれについて resolveName を呼んで待ち合わせ、名前を得てから次の id を取りだして…… と動く。つまり resolveName にかかる時間、掛ける id の数の時間がかかってしまう。

そしてこれ、次のように書くと resolveName を「並列に」呼び出せることがわかった。

val ids: List<Long> = ...
val names = ids.map { id -> async { resolveName(id) } }.awaitAll()

先の形は id をそのまま文字列に変える関数を適用して、一息に文字列のリストを取っている。
一方で後者は、 id リストを Deferred オブジェクトのリストに変換して(「いつか」実行が終わって値を取り出すことができるという計算の遅延を表現するのが Deferred オブジェクト)、 awaitAll() を通してリスト内の Deferred オブジェクトがすべて実行を完了するまで待ち合わせて値(ここでは文字列)を取り出す(結果として値のリストになる)。

各 Deferred オブジェクトは、それぞれ独立した実行コンテキストで動作するため、各 id に対する resolveName はほぼ同時に動く。結果として実行の待ち時間は(計算機の能力が十分にあれば)1回分の resolveName 呼び出しの時間にほぼ等しくなる。

* * *

resolveName の実行時間が、 id によってバラツキがあるとして、名前解決が終わった順に処理を進めたいなら次のように channel を使う方法もある。

val ids: List<Long> = ...
val channel = Channel<String>()
val jobs = ids.map { id -> launch { channel.send(resolveName(id)) } }
val names = mutableListOf<String>()
repeat(jobs.size) { names.add(channel.receive()) }

launch は Job(これもそれぞれの実行コンテキストを持つ)を起動する。
id のリストを Job のリストに変換する。
各 Job は変換結果を channel に送信していく。
このジョブの数だけ channel から受信した値を、結果の names に追加していく。

async した結果を awaitAll() する方法だと順序は保存されるけれど、 channel を使う方法だと解決できた順に格納されるので注意。(というか、それが目的で channel を使っている)

この記事が気に入ったらサポートをしてみませんか?