Developerの説明…ここだけだと理解しにくいのではじめから見ていこうと思います
https://developer.android.com/kotlin/coroutines?hl=ja
Coroutine(コルーチン)はある処理を中断/再開できるインスタンス…といった方が理解しやすいと思いました
CoroutineはThreadより軽量
事前準備
実際どのスレッドで動作しているのか確認するために、ログ出力に以下を使用します
inline fun Log(message: String) = android.util.Log.d(
"TestCoroutine",
"${Thread.currentThread().name ?: ""}[${Integer.toHexString(Thread.currentThread().id.toInt()).uppercase().padStart(4, '0')}] $message")
Coroutine Scope
CoroutineはいずれかのCoroutine Scopeに属してます
Coroutine ScopeはCoroutineが所属する仮想的な領域です
用意されているCoroutine Scope
CoroutineScope | 基底 |
GlobalScope | アプリ全体 delegate apiなのでメモリリークなどのリスクがあるので使い所を考える必要がある |
MainScope | Dispatchers.Main / SupervisorJob |
レイヤーに属しているライフサイクル対応のCoroutine Scope
lifecycleScope | Activity / Fragmentで使用可能 onDestroy()で自動キャンセル |
viewModelScope | ViewModelで使用可能 ViewModel消滅時に自動キャンセル |
以下のように使用する
CoroutineContextとlaunch / asyncなどのCoroutine Builderは後ほど
val scope = CoroutineScope(EmptyCoroutineContext)
val job = scope.launch {
Log("launch")
}
val job2 = scope.async {
Log("async")
}
Scope Builder
runBlocking | 起動された全てのCoroutinesが完了するまで自身を実行したスレッドをブロック |
coroutineScope | suspend functionで使用可能 |
supervisorScope | suspend functionで使用可能 / SupervisorJob |
// 戻値がない場合は<Unit>は省略可能
fun test() = runBlocking<Unit> {
Log("runBlocking")
}
suspend fun test() = coroutineScope<Unit> {
Log("coroutineScope")
}
Coroutine Builder
Coroutineの実装で使用
launch | |
async / await | 戻値がある場合 |
suspend fun test() = coroutineScope {
launch { Log("launch") }.join()
async { Log("async/await") }.await()
}
CoroutineContext
CoroutineContext
には以下のElementsを渡せます
Job | Job() SupervisorJob() |
CoroutineDispatcher | Dispatchers.Default Dispatchers.Main Dispatchers.IO Dispatchers.Unconfined |
CoroutineName | CoroutineName(“…”) |
CoroutineExceptionHandler | runBlockingでは使用できません |
複数指定する場合は以下のように+でつなげます
CoroutineScope(Job() + Dispatchers.Default).launch {...}
Job
キャンセル可能で階層を持つことができます
子のJobにキャンセルが発生した場合に親のJobに通知されないようにするにはSupervisorJob
を使用します
CoroutineDispatcher
Dispatchers.Default | バックグラウンドスレッドの共有プールを使用でスレッド最大数はCPUコアの数 |
Dispatchers.Main | メインスレッド |
Dispatchers.IO | ファイルのI/O、RoomなどのDB処理に使用 |
Dispatchers.Unconfined | 呼び出されたスレッド上で動作 通常使用しない |
CoroutineName
Kotlinデバックが有効時にスレッド名に指定したNameを表示しますCoroutineName()
で指定します
Kotlinデバックを有効にするには以下を呼び出します
System.setProperty(DEBUG_PROPERTY_NAME, DEBUG_PROPERTY_VALUE_ON)
// D/TestCoroutine: DefaultDispatcher-worker-1 @test#1[0038] CoroutineScope
CoroutineScope(CoroutineName("test")).launch { Log("CoroutineScope") }
CoroutineExceptionHandler
CoroutineScope
などを使用時にExceptionをハンドリングするために指定します
val handler = CoroutineExceptionHandler {context, exception ->
Log("$exception")
}
CoroutineScope(handler).launch { throw Exception("test!!!") }
実装
キャンセル/再開
Startボタンを押してループを開始してCancelボタンで停止させる実装です
scope.cancel()を実行した場合scopeを作り直さないと再開できないためjobをcancelしてます
withContextはCoroutineContextを切り替えるために使用します
delayはキャンセルを受け付けるのでyieldは不要ですが説明のためyieldも呼び出しています
cancelが受け付けられるとJobCancellationExceptionが発生します
この実装の場合画面が回転などActivityが再作成された場合はcancelされます
jobをsaveableにすればよさそうですがscopeが再作成されるためExceptionとなります
@Composable fun AppContent() {
MaterialTheme {
Surface {
val scope = rememberCoroutineScope()
var job: Job? by remember { mutableStateOf(null) }
Row {
Button(onClick = {
if (job == null || (job!!.isCancelled && job!!.isCompleted)) {
job = scope.launch {
withContext(Dispatchers.Default) {
try {
repeat(100) {
Log(it.toString())
delay(100)
// Coroutineに実行を譲渡
yield()
}
} catch (e: Exception) {
Log(e.toString())
}
}
}
}
}) {
Text("Start")
}
Button(onClick = { job?.cancel() }) {
Text("Cancel")
}
}
}
}
}
Jobの状態は以下のように遷移します
State | isActive | isCompleted | isCancelled |
---|---|---|---|
New (optional initial state) | false | false | false |
Active (default initial state) | true | false | false |
Completing (transient state) | true | false | false |
Cancelling (transient state) | false | false | true |
Cancelled (final state) | false | true | true |
Completed (final state) | false | true | false |
runBlocking
runBlockingあれこれ
はじめrunBlockingの動作から追っていこうとしたけど逆に理解しにくいと思ったのでやめた名残…
runBlocking
をそのまま使用した場合実行したスレッドで実行されます
class MainActivity : ComponentActivity() {
// D/TestCoroutine: main[0002] runBlocking
runBlocking { Log("runBlocking") }
thread {
// D/TestCoroutine: Thread-2[0038] runBlocking
runBlocking { Log("runBlocking") }
}
}
これは引数のCoroutineContext
がEmptyCoroutineContext
で動作しているためです
// D/TestCoroutine: main[0002] runBlocking
runBlocking(EmptyCoroutineContext) { Log("runBlocking") }
runBlocking
は起動された全てのCoroutinesが完了するまで自身を実行したスレッドをブロックします
そのため以下のようにDispatchers.Mainを指定した場合スレッドがロックされて次の処理まで進まなくなります
class MainActivity : ComponentActivity() {
// lockされる
runBlocking(Dispatchers.Main) { Log("Main") }
}
違うスレッドのDispatcherを使います
// D/TestCoroutine: DefaultDispatcher-worker-1[0039] Default
runBlocking(Dispatchers.Default) { Log("Default") }
// D/TestCoroutine: DefaultDispatcher-worker-1[0039] IO
runBlocking(Dispatchers.IO) { Log("IO") }
// D/TestCoroutine: main[0002] Unconfined
runBlocking(Dispatchers.Unconfined) { Log("Unconfined") }
Android Studio Dolphin 2021.3.1 Patch 1 built on September 30, 2022