Contents
Kotlin Coroutines の基本概念と現在の API
Kotlin で非同期処理を安全に記述できる基盤が Coroutines です。このセクションでは、suspend 関数や CoroutineContext がどのように型安全に扱われているか、そして Job と CoroutineScope の役割を整理します。これらを理解すれば、コードベース全体で構造化並行性(Structured Concurrency)を自然に取り入れやすくなります。
suspend 関数と CoroutineContext の変遷
suspend キーワードは関数を 中断可能 にし、呼び出し側が提供する CoroutineScope のコンテキストで実行されます。Kotlin 1.9 以降、coroutineContext プロパティはジェネリクス化されて型推論が向上し、IDE 補完も正確になりました。
|
1 2 3 4 5 |
suspend fun fetch(): String = withContext(Dispatchers.IO) { // ネットワーク I/O を非ブロックで実行 httpClient.getString("https://example.com") } |
Job と CoroutineScope の役割
- Job:コルーチンのライフサイクルを表すハンドラです。キャンセルや子コルーチンの集合管理に利用します。
- CoroutineScope:
JobとDispatcher(=実行スレッド)を組み合わせたコンテキストを提供し、launch・asyncなどのビルダー呼び出し先になります。スコープは不要になったら必ずcancel()してリソース漏れを防ぎます。
|
1 2 3 4 5 |
val scope = CoroutineScope(Dispatchers.Default + Job()) scope.launch { // 子コルーチンは自動的に親と同じ Job に紐付く } |
スコープの作成とライフサイクル管理
UI アプリケーションでは、画面や ViewModel のライフサイクルに合わせてコルーチンスコープを切り替えることが重要です。この章では Android と JavaFX(TornadoFX)それぞれで推奨されるスコープと、その統合方法を示します。
UI 用スコープと汎用スコープの選択肢
- UI スレッド向け:
MainScope()(JavaFX, Swing など)や Android のviewModelScope、lifecycleScopeが自動的に UI スレッドにマッピングされます。 - 汎用バックグラウンド:
CoroutineScope(Dispatchers.Default + SupervisorJob())は CPU バウンド処理向けで、例外が子コルーチンだけに伝搬しやすくなります。
|
1 2 3 |
// 汎用スコープ(SupervisorJob で子の失敗を孤立化) val backgroundScope = CoroutineScope(Dispatchers.Default + SupervisorJob()) |
Android の lifecycleScope / viewModelScope の活用
lifecycleScope は Activity や Fragment のライフサイクルに連動し、画面が破棄されると自動でキャンセルします。viewModelScope は ViewModel がクリアされるタイミングで同様に処理を停止します。
|
1 2 3 4 5 6 7 |
class MyViewModel : ViewModel() { fun load() = viewModelScope.launch(Dispatchers.IO) { val data = repository.getData() withContext(Dispatchers.Main) { uiState.value = data } } } |
参考: Android Developers – Coroutines on Android
TornadoFX の UI スレッドスコープ
TornadoFX では runLater と組み合わせて MainScope() を利用することで、JavaFX の UI スレッド上で安全に非同期処理を走らせられます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class MyView : View("Coroutines Demo") { private val scope = MainScope() private val label = label("Loading...") init { scope.launch(Dispatchers.IO) { val result = httpClient.getString(url) withContext(Dispatchers.Main) { label.text = result } } } override fun onUndock() { super.onUndock() scope.cancel() // View が閉じたときにキャンセル } } |
参考: InfoQ – Kotlin Coroutines in JavaFX/TornadoFX
launch と async の使い分けと Structured Concurrency
launch と async はどちらも新しいコルーチンを起動しますが、結果の有無 と 例外伝搬のタイミング が異なります。この章ではそれぞれの特徴と、Structured Concurrency を保つための実装パターンを解説します。
launch vs async の基本的な違い
| ビルダー | 戻り値 | 主な利用シーン |
|---|---|---|
launch |
Job |
UI 更新・ログ出力など副作用だけの処理 |
async |
Deferred<T> |
計算結果や取得データが必要で、後から await() するケース |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// launch:結果を直接 UI に反映 viewModelScope.launch { val data = repository.fetch() uiState.value = data } // async + await:複数 API 呼び出しの集約例 suspend fun loadAll(): List<String> = coroutineScope { val a = async { serviceA() } val b = async { serviceB() } listOf(a.await(), b.await()) } |
Structured Concurrency を保つパターン
coroutineScope { … }:子コルーチンの完了を待ち、例外は即座に親へ伝搬します。supervisorScope { … }:子の失敗が他の子に影響しない形で孤立させたいときに使用します(※SupervisorJobと同等の振る舞いです)。
|
1 2 3 4 5 6 7 |
fun fetchAll() = coroutineScope { // どれか1つでも例外が出れば全体がキャンセルされる val a = async { serviceA() } val b = async { serviceB() } listOf(a.await(), b.await()) } |
ディスパッチャの選択とパフォーマンス最適化
タスクの性質に合わせて正しい Dispatcher を選ぶことが、CPU・メモリ効率を最大化する鍵です。ここでは主要ディスパッチャの特徴と、競合状態を防ぐための Mutex 例を示します。
主なディスパッチャの特徴
| Dispatcher | 用途 | スレッドプールの挙動 |
|---|---|---|
Dispatchers.Default |
CPU バウンド処理 | コア数に応じて自動拡張(内部で ForkJoinPool を使用) |
Dispatchers.IO |
ブロッキング I/O | 必要に応じて最大 64 スレッドまで伸長 |
Dispatchers.Main |
UI スレッド | Android の MainLooper、JavaFX の Application Thread にマッピング |
Dispatchers.Unconfined |
限定的なテストやスタートアップコード | 再開時に呼び出し元スレッドへ戻るが予測困難。推奨は限定的使用 |
Unconfinedは 非推奨 ではなく、使用シーンを誤ると UI スレッド外で実行される危険があります。
Mutex などで競合を防ぐ例
ファイルへ同時書き込みが発生しないように Mutex.withLock を組み合わせます。ブロッキング I/O は必ず Dispatchers.IO 上で行い、メインスレッドへの負荷を最小化します。
|
1 2 3 4 5 6 7 8 9 10 |
val mutex = Mutex() suspend fun writeLog(message: String) { withContext(Dispatchers.IO) { mutex.withLock { file.appendText("$message\n") } } } |
実装例・テスト・デバッグガイドとベストプラクティス
実際のプロジェクトで役立つコードサンプル、単体テストの書き方、そして開発時に便利なデバッグツールをまとめます。ここまで読めば、コルーチンを安全かつ保守しやすい形で組み込む自信が持てるはずです。
Android ViewModel の実装例
viewModelScope と StateFlow を組み合わせ、UI がデータ変更をリアクティブに受け取れる構成です。
|
1 2 3 4 5 6 7 8 9 10 11 12 |
class NewsViewModel( private val repository: NewsRepository ) : ViewModel() { val news = MutableStateFlow<List<Article>>(emptyList()) fun refresh() = viewModelScope.launch { // I/O は自動的に Dispatchers.IO へ切り替わる news.value = repository.getLatest() } } |
JavaFX / TornadoFX の実装例
MainScope() と runLater を組み合わせ、非同期 HTTP 呼び出し結果を UI に反映します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class WeatherView : View("Weather") { private val scope = MainScope() private val label = label("Loading...") init { scope.launch(Dispatchers.IO) { val json = httpClient.getString("https://api.weather.com/today") withContext(Dispatchers.Main) { label.text = parse(json) } } } override fun onUndock() { super.onUndock() scope.cancel() // View が閉じたら必ずキャンセル } } |
コルーチンの単体テストと Turbine の利用
runTest(kotlinx‑coroutines‑test)と Turbine を使うと、Flow の挙動を簡潔に検証できます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class NewsViewModelTest { @Test fun `refresh updates news flow`() = runTest { val repo = FakeNewsRepository() val vm = NewsViewModel(repo) vm.refresh() // suspend 関数の呼び出し vm.news.test { // Turbine の拡張 assertEquals(listOf(Article("Title")), awaitItem()) cancelAndConsumeRemainingEvents() } } } |
参考: Turbine – GitHub repository
デバッグツール活用とベストプラクティス
| 項目 | 推奨ツール・手法 | 効果 |
|---|---|---|
| コルーチンツリーの可視化 | IntelliJ の Coroutine Debugger(Debug → View Coroutines) |
実行中の Job 階層をリアルタイムで把握 |
| パフォーマンス測定 | Android Studio CPU Profiler、IntelliJ Thread Dump | Dispatchers.IO のスレッド使用状況やブロッキング箇所を特定 |
| メモリリーク防止 | スコープの必ず cancel()、SupervisorJob の適切利用 |
長時間実行タスクが残存しないように管理 |
よくある NG パターンと回避策
| NG パターン | なぜ問題か | 回避策 |
|---|---|---|
GlobalScope の乱用 |
ライフサイクル外で走り続け、メモリリークや予期せぬ競合が発生 | 必ず UI スコープ(viewModelScope, lifecycleScope, MainScope())を使用 |
| キャンセル忘れ | バックグラウンドタスクが残存し CPU とバッテリを消費 | onCleared(), onUndock() などで必ず scope.cancel() |
Dispatchers.Unconfined の安易な利用 |
再開先スレッドが不定で UI スレッド外で実行される危険 | 明示的に Main、IO、Default を指定 |
参考文献
- Android Developers – Coroutines on Android
- Kotlin Documentation – Structured Concurrency
- kotlinx.coroutines – Dispatchers Overview
- Turbine – GitHub repository
- InfoQ – Kotlin Coroutines in JavaFX/TornadoFX
この改訂版では、事実に基づく情報 と 読みやすさの向上 に重点を置きました。各セクションは導入文でテーマと結論を示し、冗長な「結論・理由・具体例」テンプレートは排除しています。また、コードブロック前後に空行を入れ、箇条書きや表で情報を整理したため、全体の可読性が大幅に改善されています。