0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Kotlin】Kotlin Coroutinesで理解する構造化並行性(Structured Concurrency)

Posted at

はじめに

「コルーチンの混沌を、スコープで制御する」


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などで採用

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?