はじめに
Kotlin Coroutineは非常に軽量であり、多数のCoroutineを簡単に起動できます。
一方で、外部APIやデータベースなどのI/O処理には必ず上限があり、無制限に並列実行するとパフォーマンス低下や障害につながることがあります。
本記事では、kotlinx.coroutinesが提供するSemaphoreを使い、Coroutineの同時実行数を制御する方法を整理します。
なぜ同時実行数を制御する必要があるのか
Coroutineはスレッドではないため、launchを大量に呼び出してもすぐに問題が顕在化しないことがあります。
しかし、以下のようなケースでは並列数の制御が重要になります。
- 外部APIの同時接続数に制限がある
- DBコネクション数が有限である
- 下流サービスへの負荷を抑えたい
Coroutine向けのSemaphore
Coroutineの同時実行数を制御する方法として Semaphore を使ったものがあります。
Kotlin Coroutineでは、JavaのSemaphoreではなく
kotlinx.coroutines.sync.Semaphoreを使用します。
このSemaphoreは、スレッドをブロックせずにsuspendで待機できる点が特徴です。
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
Semaphoreの基本的な使い方
Semaphoreは「同時に通過できる枠」を表します。
val semaphore = Semaphore(permits = 5)
suspend fun tame() {
semaphore.withPermit {
// 同時に最大5つまでしか実行されない
feedCats()
}
}
withPermitで囲まれた処理は、許可数を超えるとsuspendされ、
空きができたタイミングで再開されます。
Coroutineと組み合わせた例
実務でよく使うのは、複数のCoroutineを起動しつつ、
実行数だけをSemaphoreで制限するパターンです。
val semaphore = Semaphore(permits = 5)
suspend fun tame() = coroutineScope {
repeat(100) {
launch {
semaphore.withPermit {
feedCats()
}
}
}
}
この例では、
- Coroutineは100個起動される
- 実際に処理が同時実行されるのは最大5個まで
という状態になります。
「起動数」と「同時実行数」を分離できる点が、Semaphoreの大きなメリットです。
DispatcherやChannelとの違い
Semaphoreは、処理の同時実行数そのものを制御したい場合に向いています。
- Dispatcher
スレッドの割り当てや実行環境を制御する - Channel
キューを介したデータ受け渡しやワーカー構成に向いている - Semaphore
単純に「同時に何個まで処理してよいか」を制限する
注意点
Semaphoreは万能ではありません。
- CPUバウンドな処理にはDispatcher調整の方が適している
- 外部I/Oや重い処理の並列数制御に向いている
- 制限値は下流サービスの特性を考慮して決める必要がある
適切な場所で使うことで、安定した非同期処理を実現できます。
おわりに
Coroutineは軽量で扱いやすい反面、並列数を意識しないと簡単に過負荷を招きます。
Semaphoreを使えば、Coroutineの利便性を保ったまま、安全に同時実行数を制御できます。
特に外部I/Oを伴う処理では、同時実行数を適切に制御する必要があります。