はじめに
「コルーチンの混沌を、スコープで制御する」
1. 構造化並行性とは?
Structured Concurrency(構造化並行性) とは、
コルーチンの ライフサイクル(開始・終了・キャンセル)を構造化して管理する考え方です。
Kotlin公式が採用している設計思想で、
「親が生きている間だけ子が動き、親が終われば全て終わる」
という明確なスコープ構造を保証します。
非構造的(Bad)な並行処理
fun launchGlobal() {
GlobalScope.launch {
delay(1000)
println("完了!")
}
}
この場合:
-
GlobalScopeはアプリ全体に属するため、どこからもキャンセルできません - 関数が終わってもコルーチンが動き続け、メモリリークの温床になります
構造化された(Good)並行処理
suspend fun loadData() = coroutineScope {
launch {
delay(1000)
println("ユーザー情報取得完了")
}
launch {
delay(500)
println("注文情報取得完了")
}
}
coroutineScopeにより、
すべてのlaunchは「この関数のスコープ」に属します。
関数が終われば、自動的に子コルーチンもキャンセルされます。
2. なぜ構造化が必要なのか?
並行処理が増えるほど、次のような問題が発生します:
| 問題 | 説明 |
|---|---|
| キャンセル漏れ | 親が終了しても子が動き続ける |
| 例外伝播が不明瞭 | 子タスクの例外がどこに伝わるかわからない |
| メモリリーク | スコープを持たないコルーチンが永遠に残る |
| デバッグ困難 | コルーチンの親子関係が見えない |
Structured Concurrency はこれをスコープ階層で解決します。
3. スコープ階層で理解する
- すべての子コルーチンは親に属する
- 親がキャンセルされると、子も自動でキャンセルされる
- 子が例外を投げると、親がそれを受け取る(unless Supervisor)
4. coroutineScope vs supervisorScope
| 関数 | 特徴 | 子の例外時の動作 |
|---|---|---|
coroutineScope {} |
通常の構造化スコープ | 他の子も全てキャンセルされる |
supervisorScope {} |
独立したタスクを許可 | 他の子は継続可能 |
例:coroutineScope
suspend fun example() = coroutineScope {
launch {
throw Exception("Aでエラー発生")
}
launch {
delay(1000)
println("B完了(※実行されない)")
}
}
出力:
Exception: Aでエラー発生
→ Aの失敗によりスコープ全体がキャンセルされ、Bは実行されません。
例:supervisorScope
suspend fun example() = supervisorScope {
launch {
throw Exception("Aでエラー発生")
}
launch {
delay(1000)
println("B完了(継続)")
}
}
出力:
Exception: Aでエラー発生
B完了(継続)
→ supervisorScopeでは他の子タスクが影響を受けずに動き続けます。
5. SupervisorJob と組み合わせる
アプリ全体のルートスコープ(例:ViewModel, Serviceなど)で、
SupervisorJobを使うことで局所的なエラー分離が可能になります。
val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
scope.launch {
// 片方が失敗しても他は続行
launch { fetchUser() }
launch { fetchOrders() }
}
Androidの
ViewModelScopeも、内部的にこの設計を採用しています。
6. Structured Concurrency の3原則
| 原則 | 意味 |
|---|---|
| 親子構造 | 子は必ず親スコープの中で動く |
| ライフサイクル一致 | 親が終われば子も自動終了 |
| 明確なエラープロパゲーション | 例外は親に伝播 or Supervisorで隔離 |
7. 実践:複数タスクの構造化並行処理
suspend fun loadAll() = coroutineScope {
val user = async { fetchUser() }
val orders = async { fetchOrders() }
val address = async { fetchAddress() }
println("結果: ${user.await()}, ${orders.await()}, ${address.await()}")
}
fun main() = runBlocking {
try {
loadAll()
} catch (e: Exception) {
println("エラー捕捉: ${e.message}")
}
}
- どれかのタスクが失敗すれば全体がキャンセルされる
- 成功すれば全ての結果を結合できる
8. よくある誤解
| 誤解 | 実際 |
|---|---|
| 「GlobalScopeでlaunchすれば便利」 | ❌ スコープ外のタスクが制御不能になる |
| 「launchを使えば並列になる」 | ❌ 並行処理。CPU並列化には Dispatchers.Default が必要 |
| 「delay()はスレッドをブロックする」 | ❌ ブロックせず、他のタスクに切り替える |
| 「スコープをネストしても問題ない」 | ⚠️ 深いネストは可読性を損なう。明確な責務を設計する |
9. 図で理解する構造化並行性
各スコープが「親スコープ」に属し、
親がキャンセルされれば自動的にすべて停止します。
10. まとめ
| 要点 | 内容 |
|---|---|
| 構造化並行性とは | コルーチンのライフサイクルをスコープで明確に管理する設計 |
| 利点 | 安全なキャンセル、明確な例外伝播、メモリリーク防止 |
| 主要API |
coroutineScope / supervisorScope / SupervisorJob
|
| ベストプラクティス | GlobalScopeを避け、親子スコープで責務を明確化する |
| 現場応用 | AndroidのViewModelScope, lifecycleScope, Server-side CoroutineScopeなどで採用 |