はじめに
公式とGoogle Codelabsを参考にKotlinのcoroutineについて学習したので
メモとして残しておきます。
環境
-
Windows 10 Home (1903)
-
kotlin (1.3.72)
-
kotlinx-coroutines-core (1.3.4)
参考記事
Your first coroutine with Kotlin
-
coroutineはkotlinの言語レベルでサポートされている非同期プログラミングなどを実行する新しい手法(threadのような実行単位の1つと解釈した)
-
coroutineはthreadの共有プール上で実行され、並列に処理される
-
1つのthreadで多くのcoroutineを実行することができるのでthreadを生成するよりコストが低い
-
coroutineはthreadをブロックせずにcoroutineのみをsuspend/resumeできる
-
threadをブロックせず、coroutine内から呼び出す関数にはsuspend修飾子を付ける
-
suspend関数はcoroutineかsuspend関数からのみ呼び出すことができる
val deferred = (1..1_000_000).map { n ->
GlobalScope.async {
delay(1000) // (1)
n
}
}
runBlocking {
val sum = deferred.map { it.await().toLong() }.sum() // (2)
println("Sum: $sum")
}
-
coroutineを1,000msec停止する(≒1,000,000sec実行スレッドでかかるわけではない)
-
coroutineの結果を待ち合わせる。10sec程で完了するのでcoroutineが並列処理されているのがわかる
Asynchronous Programming Techniques
Threading
-
context switchingが発生するためコストが高い
-
競合によりデバッグが難しい
Callbacks
-
callbackがネストしていくことで可読性が低下する
-
エラー処理が複雑になる
Futures, Promises et. al
-
戻り値はPromise型になり、実際のデータを直接指定することができない
-
エラー処理が複雑になる(エラーの伝搬方法が直感的ではない気がする)
Reactive Extensions
-
多くのプラットフォームに移植されているので、一貫したAPIを利用できる
-
新しい概念を導入することになるのでコストは高くなる
Coroutines
-
同期コードを書くのと同様にtop-downで記述できる
-
エラー処理は既存のプログラミング方法と変える必要がない
Introduction to Coroutines and Channels
solutionブランチのコードを見ながらざっと流し読みした。
-
coroutine scopeとcoroutine contextという概念がある
-
coroutine scopeは構造化されており親子関係を持つ
-
同じcoroutine scopeで実施されている場合、親coroutineがキャンセルされると子coroutineも自動的にキャンセルされる
-
channelを使用して別々のcoroutine間で通信を行うことができる
Use Kotlin Coroutines in your Android App
-
suspend関数はコードが実行されるthreadを指定する訳ではないのでmain threadでも実行できる
-
androidxのlifecycle-viewmodel-ktxライブラリにはDispathcers.MainにバインドされViewModelがクリアされた時にキャンセルされるviewModelScopeが追加されている
-
coroutineのUnit Testはcoroutineのタイマー操作を行うことがdelayさせた時間を早めて実行することができ、実際にdelayさせる時間分待つ必要はない
-
launchやasyncを使ってcoroutineを開始できる
-
結果を返す必要がない場合はlaunchを使い、結果を返す必要がある場合はasyncを使う
-
子coroutineが例外を投げた場合、デフォルトで親coroutineもキャンセルされる
-
CoroutineExceptionHandlerを使ってcoroutine内でcatchされなかった例外が発生した時の動作をカスタマイズできる
-
IO dispatcherはネットワークやディスクからの読み込みといったIOに最適化されている
-
Default dispatcherはCPUを集中的に使用するタスクに最適化されている
-
RoomもRetrofitもsuspend関数をmain-safeにしているので、Dispathcers.Mainから呼び出しても安全(main threadをブロックしない)
-
main-safeなsuspend関数であればDispatcher.Mainから呼び出せる(withContextを使う 必要がない)
-
慣習として自分で用意したsuspend関数はmain-safeであることを保証する
-
テスト関数を抜けた時点でテストが終了してしまうため、coroutineが完了するまでテスト実行threadが待つようにrunBlockingTestでcoroutineを開始する必要がある
@Test
fun whenRefreshTitleSuccess_insertsRows() {
val subject = TitleRepository(
MainNetworkFake("OK"),
TitleDaoFake("title")
)
// launch starts a coroutine then immediately returns
GlobalScope.launch {
// since this is asynchronous code, this may be called *after* the test completes
subject.refreshTitle()
}
// test function returns immediately, and
// doesn't see the results of refreshTitle
}
-
refreshTitleが実行されたかどうかを知らずにテストを終了してしまう
-
refreshTitleが例外を投げた場合、テストコールスタックには投げられず、GlobalScopeの例外ハンドラでuncaught exceptionとして扱われる
-
withTimeout functionを使用することでcoroutineのタイムアウトを設定できる
-
advanceTimeBy functionを使用することでcoroutineの仮想時間を指定した時間だけ勧めることができるので、タイムアウトのテストに利用できる
感想
Coroutineを利用することでアプリのパフォーマンスが向上するだけでなく
可読性も大幅に上がるので、これを機に利用していこうと思います。