はじめに
Kotlinのコルーチン、私はスレッドに対する明確なメリットを知らず、あまり使ってませんでした。
しかし今回、大量のレコードをなるべく速く処理したいという状況で、コルーチンがとても役に立ったので紹介します。
マルチコアを使い切る
最近のCPUはたいてい複数のコアを持っていますが、シングルスレッドのプログラムでは1つのコアしか使うことができません。
しかし、並列で処理できるタスクなら全てのCPUコアを使って並列処理するのが一番速いです。
全てのCPUコアを使って並列処理するには、コアの数だけスレッドを作り、それらのスレッドを上手く管理して並列に処理をする必要がありますが、マルチスレッドプログラミングは落とし穴が多く、自分で実装すると意外と時間がかかるものです。
しかし、Kotlinのコルーチンを使えばそれを簡単に実現することができます。
コードサンプル
コードから入ります。
以下の数行のコードでCPUコアを全て使って並列処理ができます。
import kotlinx.coroutines.*
fun <T> operateItems(items: Collection<T>, operate: (T) -> Unit) = runBlocking(Dispatchers.Default) {
items.map {
async { operate(it) }
}.awaitAll()
}
簡単にコードの解説をすると、例えば、10万件のレコードを並列処理したい場合、引数items
に10万件のレコードを渡し、引数operate
に各レコードの処理をする関数を渡します。
各レコードについて、async
関数で作られたコルーチンで並列処理が行われ、awaitAll
で全ての処理が終わるまで待機します。
コルーチンのスレッド管理
コルーチンはDispatherというもので、スレッドの使い方を決めます。
コルーチンは必ずしもマルチスレッドで動くとは限らず、Dispather次第でシングルスレッドで動かすこともできます。
コードサンプルではDispatcherにDispatchers.Default
を使用していますが、このDispatcherはCPUコアの数だけスレッドを作ってプールし、コルーチンの処理をそれらのスレッドで分散して実行します。
これにより全てのCPUコアを使って並列処理ができます。
コードサンプルの例では、async内に書いたoperate(it)
は、CPUコアが空き次第順番に実行されていきます。
例えば、4コアのCPUならoperateがまず4回実行され、いずれかの実行が終わり次第次のoperateが実行され、常に4つのoperateが並行実行されている状態になります。
速度計測
6コアCPUのマシンで上記のコルーチンのコードを実行し、シングルスレッドの処理と速度を比較してみました。
6コアなので上手くいけばコルーチンはシングルスレッドの6倍速になるはずです。
100ミリ秒くらいかかる処理を1000回実行するパターンと、10ミリ秒くらいかかる処理を10000回実行するパターンの2パターンで速度計測しています。
operateItems((1..1000).toList()) {
processTakes100ms() // Math.pow()を大量実行して100msくらいかかるよう調整した関数
}
シングルスレッド | コルーチン | |
---|---|---|
100msの処理 × 1000回 | 99.4s | 17.4s |
10msの処理 × 10000回 | 100.4s | 17.5s |
シングルスレッドで100秒ほどかかる処理がコルーチンでは17.5秒程度で、シングルスレッドの5.7倍ほどの速度になっています。
6倍には少し届きませんでしたが、十分許容できるオーバーヘッドです。
特に後者はコルーチンを1万個も生成していますが、それでも速度があまり落ちず、軽量であるというコルーチンの強み実感することができました。
まとめ
- コルーチンのDispathcers.DefaultはCPUコアを全部使ってくれる
- コルーチンは軽いから大量生産しても大丈夫