はじめに
― 「どのスレッドで動くのか」を支配するコルーチンの心臓部 ―
1. Dispatcherとは?
CoroutineDispatcher は、
コルーチンをどのスレッド(Thread)で実行するかを決める仕組みです。
コルーチン自体は軽量スレッドのような存在ですが、
実際の実行は「Dispatcher」によってスレッドプール上で行われます。
コルーチンの流れ(概念図)
Dispatcher は “スケジューラ” のような存在。
どの CPU コア・スレッドを使うかを決定します。
2. Kotlin 標準の Dispatcher 一覧
| Dispatcher | 説明 | 主な用途 |
|---|---|---|
Dispatchers.Default |
CPUバウンド処理向けスレッドプール | 計算・圧縮・暗号化など |
Dispatchers.IO |
I/O待機処理向けスレッドプール | ファイル・DB・ネット通信など |
Dispatchers.Main |
メイン/UIスレッド | AndroidやDesktopのUI更新 |
Dispatchers.Unconfined |
スレッド未固定(呼び出し元依存) | 特殊用途・テスト用 |
3. Dispatchers の内部構造
Default
- CPUコア数に比例したスレッド数(例:8コア → 8スレッド)
- ForkJoinPoolベース(
commonPool)
async(Dispatchers.Default) { heavyCalculation() }
高負荷の並列処理を行う際の標準選択肢。
スレッド数 ≒ CPUコア数。
IO
-
Defaultより多いスレッド数を持つ(最大64) - ネットワークやファイルなど「待ち時間が多い処理」に最適
withContext(Dispatchers.IO) {
val data = URL("https://example.com").readText()
}
待ちが発生するI/O処理を「並行化」するために最適化されています。
Main
- Android / Compose Desktop でUIスレッドを操作
- Androidでは
Looper.getMainLooper()にバインド
withContext(Dispatchers.Main) {
textView.text = "更新完了!"
}
メインスレッドは UI更新専用。
重い処理は絶対にここで行わないこと!
Unconfined
- 最初は呼び出し元スレッドで実行
- 次の中断ポイント後は再開スレッドが変わることもある
launch(Dispatchers.Unconfined) {
println("開始: ${Thread.currentThread().name}")
delay(100)
println("再開: ${Thread.currentThread().name}")
}
出力例:
開始: main
再開: DefaultDispatcher-worker-1
⚠️ 予測不能な動作になるため、実務ではほぼ非推奨です。
テスト・低レベル実験向け。
4. withContext() によるスレッド切り替え
withContext(dispatcher) を使うと、
同じスコープ内でもスレッドを安全に切り替えることができます。
suspend fun fetchData() {
val json = withContext(Dispatchers.IO) {
println("I/Oスレッド: ${Thread.currentThread().name}")
URL("https://example.com").readText()
}
withContext(Dispatchers.Main) {
println("UIスレッド: ${Thread.currentThread().name}")
println("データ表示: $json")
}
}
出力:
I/Oスレッド: DefaultDispatcher-worker-2
UIスレッド: main
I/Oで取得 → MainでUI更新
という典型的な構造です。
5. Dispatcher の切り替えは軽量
withContext() は スレッドをブロックしません。
単に「次の中断ポイントを別のDispatcherで再開する」だけです。
したがって、以下のように連続切り替えしてもパフォーマンス低下はほとんどありません。
withContext(Dispatchers.IO) { ... }
withContext(Dispatchers.Default) { ... }
withContext(Dispatchers.Main) { ... }
6. よくある誤用と落とし穴
| 誤用 | 問題点 | 正しい方法 |
|---|---|---|
runBlocking(Dispatchers.IO) |
メインスレッドをブロック |
launch(Dispatchers.IO) または withContext
|
GlobalScope.launch(Dispatchers.Main) |
ライフサイクル無視 |
viewModelScope / lifecycleScope
|
Dispatchers.Unconfined |
予測不能な再開スレッド | 明示的に Default / IO を使用 |
重い処理を Main で実行 |
ANR / フリーズの原因 |
withContext(Dispatchers.Default) に移動 |
7. カスタムDispatcherを作る
val singleDispatcher = newSingleThreadContext("MyThread")
fun main() = runBlocking(singleDispatcher) {
println("スレッド名: ${Thread.currentThread().name}")
}
newSingleThreadContext()は 1つの専用スレッドを作成します。
⚠️ リソースリーク防止のため、使用後はclose()が必要です。
ExecutorベースのDispatcher
val executor = Executors.newFixedThreadPool(4)
val myDispatcher = executor.asCoroutineDispatcher()
runBlocking(myDispatcher) {
repeat(4) {
launch { println("${Thread.currentThread().name} 実行中") }
}
}
これにより、業務アプリに最適化された独自スレッドプールを実現可能。
8. スレッド切り替えの図解
コルーチンは
withContext()を通して
スレッドプール間を 安全かつ非同期に移動 します。
9. ベストプラクティスまとめ
| シーン | 推奨 Dispatcher | 理由 |
|---|---|---|
| CPU負荷(計算・圧縮) | Dispatchers.Default |
コア数最適化 |
| ネット通信・DB・ファイルI/O | Dispatchers.IO |
多スレッド許容 |
| UI更新 | Dispatchers.Main |
メインスレッド操作 |
| テスト・内部制御 | Dispatchers.Unconfined |
特殊用途のみ |
| 長時間専用スレッド | newSingleThreadContext() |
独立タスク向け |
まとめ
| 要点 | 内容 |
|---|---|
| Dispatcher とは | コルーチンのスレッドスケジューラ |
| Default | CPUバウンド(計算系) |
| IO | I/Oバウンド(待機系) |
| Main | UIスレッド(Androidなど) |
| Unconfined | 非固定スレッド(実験用) |
| スレッド切り替え |
withContext() で安全に行う |
| 最適化の鍵 | 処理の性質に合わせてDispatcherを選択 |