はじめに
〜コルーチンの「生存範囲」を制御するスコープの仕組み〜
概要(Overview)
Kotlin のコルーチンを使うとき、必ずといっていいほど登場するのが CoroutineScope。
これは一言で言えば:
「コルーチン(Coroutine)のライフサイクルを管理する器」
CoroutineScope を使うことで、
- どの範囲でコルーチンが生きるか
- どのタイミングでキャンセルするか
- どのスレッド上で動かすか
…といった「管理」を安全に行えるようになります。
コルーチンの基本構造
まず、コルーチンを動かす最小構成を見てみましょう。
import kotlinx.coroutines.*
fun main() = runBlocking {
launch {
delay(1000)
println("Hello from coroutine!")
}
}
ここで runBlocking が「スコープ(CoroutineScope)」を作っています。
そして launch { ... } が「このスコープ内で実行されるコルーチン」です。
CoroutineScope の役割
CoroutineScope は、コルーチンの構造的な親子関係(Structured Concurrency) を保つために存在します。
| 要素 | 役割 |
|---|---|
| CoroutineScope | コルーチンのライフサイクルを管理するコンテナ |
| Job | コルーチン自体の実行単位(キャンセル・完了を管理) |
| Dispatcher | コルーチンが動作するスレッドプール(IO / Main / Defaultなど) |
イメージ図
スコープが消えると、すべての子コルーチンがキャンセルされます。
これが「構造化並行性(Structured Concurrency)」の基本思想です。
CoroutineScope の作り方
1. runBlocking(メイン関数でよく使う)
fun main() = runBlocking {
println("Start")
launch {
delay(500)
println("Work done")
}
println("End")
}
runBlocking は 現在のスレッドをブロックしてコルーチンを実行。
通常はメイン関数やテストで使用。
2. CoroutineScope()(明示的に作る)
val scope = CoroutineScope(Dispatchers.Default)
scope.launch {
delay(1000)
println("Hello from my scope!")
}
Dispatchers.Default 上でコルーチンを起動。
アプリ全体やサービス単位で独自スコープを持ちたいときに使用。
3. MainScope()(UIアプリなど)
class MyViewModel : ViewModel() {
private val scope = MainScope()
fun loadData() {
scope.launch {
delay(1000)
println("Data loaded")
}
}
override fun onCleared() {
scope.cancel() // 🔥 スコープ終了時にキャンセル
}
}
Android / Compose / ViewModel でよく使うパターン。
UI破棄時に自動キャンセルが可能。
CoroutineScope のキャンセル
CoroutineScope には「キャンセル連鎖」があります。
親スコープがキャンセルされると、子コルーチンもすべて停止します。
val scope = CoroutineScope(Dispatchers.Default)
val job = scope.launch {
repeat(5) {
println("Working $it ...")
delay(500)
}
}
Thread.sleep(1000)
scope.cancel() // 🔥 親スコープをキャンセル
出力:
Working 0 ...
Working 1 ...
(キャンセルで終了)
cancel() すればすべての子がまとめて停止。
メモリリーク防止・安全な終了処理が可能。
スコープとコンテキスト(Context)
CoroutineScope は CoroutineContext を持っています。
コンテキストには主に以下が含まれます:
| 要素 | 型 | 説明 |
|---|---|---|
| Job | Job |
コルーチンのキャンセル・完了管理 |
| Dispatcher | CoroutineDispatcher |
実行スレッド指定(IO, Default, Main) |
| ExceptionHandler | CoroutineExceptionHandler |
例外ハンドリング |
例:複数設定をまとめてスコープ生成
val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
複数子コルーチンを独立して実行するための SupervisorJob() も利用可能。
構造化並行性(Structured Concurrency)
CoroutineScope があるおかげで、
「コルーチンの生存範囲」「エラー伝播」「キャンセル制御」がすべて自動化されます。
スコープがないコード(危険)
GlobalScope.launch { delay(1000); println("Leaked!") }
GlobalScope はアプリ終了まで生き続ける(リークリスクあり)
スコープを使った構造化
suspend fun doWork() = coroutineScope {
launch { delay(1000); println("A done") }
launch { delay(500); println("B done") }
}
coroutineScope {} 内で起動した子は、すべて完了するまで待機。
安全・明確な並行構造。
コード例:親子スコープの違い
import kotlinx.coroutines.*
fun main() = runBlocking {
println("Start")
launch {
delay(1000)
println("Child coroutine finished")
}
println("End of runBlocking")
}
runBlocking が終了するとき、 中の launch が完了するまで自動的に待機します。
これが「構造化されたスコープ管理」です。
よく使うスコープ一覧
| スコープ名 | 用途 | スレッド動作 |
|---|---|---|
runBlocking |
メイン・テストコード | 呼び出しスレッドをブロック |
GlobalScope |
永続的なバックグラウンド処理(非推奨) | アプリ終了まで生存 |
CoroutineScope() |
任意のスコープ定義 | 任意のDispatcher |
MainScope() |
UI系クラス(ViewModel, Activity) | Mainスレッド |
coroutineScope {} |
suspend関数内で一時的に使う | 呼び出しスレッドと連動 |
まとめ
| 概念 | 説明 |
|---|---|
CoroutineScope |
コルーチンのライフサイクルを管理する枠組み |
launch / async
|
スコープ内でコルーチンを起動 |
cancel() |
スコープ全体をキャンセル可能 |
Dispatcher |
実行スレッドを指定(Main / IO / Default) |
| 構造化並行性 | 親スコープが子コルーチンを安全に管理する仕組み |