この記事は社内のエンジニアが集まるScrapboxへ書いたページの転記です。
なんとなくKotlin Coroutinesについて理解が深まると幸いです。
参考URL
- https://kotlinlang.org/docs/reference/coroutines/coroutines-guide.html
- Kotlin コルーチンを 理解しよう 2019 - KotlinFest2019 - - Speaker Deck
- Using Kotlin Coroutines in your Android App
Coroutineを使うにあたって重要な要素
Suspending Function
- 中断可能な関数
- Coroutineの基礎となる関数
- Suspending FunctionはSuspending Functionの中でしか呼べない
- 通常の関数記法にsuspendをつけるとできあがり
- Suspending Functionそのものは直列で実行される
- → 中断することができる
- 実はJavaのコードでも書ける
- Roomのコード生成がSuspending Functionに対応してるのはこれ
- https://kotlinlang.org/docs/reference/coroutines/composing-suspending-functions.html#composing-suspending-functions
CoroutineDispatcher
- Coroutineの実行するスレッドを決める仕組み
- RxでいうSchedulerみたいなもの
- デフォルトでDsipatchersに3種類のDispatcherが用意されている
- Main
- Default
- IO
- IO用のDispatcher
- Defaultとスレッドプールが共有されるが、こちらはSystemのpropertyで数を制限できる
- Defaultとの違いはAndroid上では名前くらいしかないのでDefault使えばいい気がしてる
CoroutineContext
- Coroutineの情報を詰め込んでいるオブジェクト
- 要素がある
- CoroutineInterceptor
- ほぼCoroutineDispatcher
- ExceptionHandler
- catchされなかった例外のハンドリングの仕組み
- デフォルトだと何も処理されない
- Androidの場合はAndroidExceptionPreHandlerが使われてスレッドに設定されているUncaughtExceptionHandlerに処理が以上される
- →手でハンドリングしないとアプリがエラーで落ちる
- Androidの場合はAndroidExceptionPreHandlerが使われてスレッドに設定されているUncaughtExceptionHandlerに処理が以上される
- https://kotlinlang.org/docs/reference/coroutines/exception-handling.html
- Job
- 現在のCoroutineのJob
- 子のCoroutineを作る時はこのJobの子として作成される
- cancelを呼ぶとキャンセルされる
- 子のCoroutineも全てキャンセルされる
- CoroutineName
- Coroutineに名前をつける仕組み
- デバッグの時に便利だろうけどあんまり使わない
- その他色々あるけど実際に使うのはこれくらい
- Coroutine内部では色々使われている模様
- CoroutineInterceptor
CoroutineScope
- Coroutineを実行するためのスコープ
- CoroutineContextをもつ
- Coroutineを作成するためのエンドポイントになる拡張関数(コルーチンビルダーと呼ばれている)が用意されている
async
launch
- スコープ内で生成されたCoroutineはスコープのCoroutineContextを継承する
- →
Dispatchers.Main
がDispatcherとして設定されていれば作成されたCoroutineはメインスレッド上で実行される
- →
- CoroutineScopeはネストできる
- catchしていない例外やキャンセルが起きた時に並行実行されている他の処理がキャンセルされる単位
- 親のScopeに例外は伝搬するがcatchすればキャンセルされない
SupervisorJob
やsupervisorScope
を使えば親から子への一方向にしか伝播しないようにできる- https://kotlinlang.org/docs/reference/coroutines/exception-handling.html#supervision
- 子で起きた例外が親に伝播しない
- 特にUIコンポーネント用のスコープとして有用
- 実例としてAndroid Jetpackの
viewModelScope
やlifecycleScope
はSupervisorJob
が使われているので、子から親には伝播しないようになっている
コルーチンを書く
1.CoroutineScopeを作る、既にあるものを使う
CoroutineScopeを作る
CoroutineScopeを作るには以下のような関数で行える
CoroutineScope
- https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope.html 一番基本的な関数、引数にCoroutineCointextを渡すと特筆すべきこともないCoroutineScoreができあがる あまり使わない気がする
MainScope
- https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-main-scope.html
SupervisorJob
とDispatchers.Main
をもっているCoroutineScope UIを操作する時に使ったりする ActivityやFragmentなどで使うことがあったが、lifecycle-runtime-ktx
に追加されたLifecycleCoroutineScope
の登場で使わなくなっていきそう
- https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-main-scope.html
coroutineScope
- https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-global-scope/index.html Suspending function内で新しいCoroutineScopeを立ち上げる関数 引数に渡したラムダ式の内容を実行して戻り値を受け取れる 特定の処理の中で一部だけScopeを切りたい時に使う
- 主に複数の
async {}
を並行実行したい時 よく使う
既にあるものを使う
既にあるものを使うと便利、よく使うCoroutineScopeは以下の通り
ViewModel.viewModelScope
- ViewModelに用意されているCoroutineScope
SupervisorJob
+Dispatchers.Main
- 最近は
Dispatchers.Main.immediate
になった模様
- 最近は
ViewModel.onCleared
実行時にキャンセルされる- ViewModel内でコルーチンを作りたい時は基本的にこれを使うことになりそう
Lifecycle.coroutineScoope
/LifecycleOwner.lifecycleScope
androidx.lifecycle
のLifecycle
と連動しているCoroutineScopeandroidx.lifecycle.Lifecycle.Event#ON_DESTROY
のイベントを受け取った時にキャンセルされる- Activity/Fragmentだと
onDestroy
Fragment.viewLifecyclerOwner
だとFragment.onDestroyView
- Activity/Fragmentだと
launchWhenCreated
/launchWhenResumed
/launchWhenResumed
といった便利コルーチンビルダーが生えている
GlobalScope
- https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-global-scope/index.html
- デフォルトで用意されているやつ
- 特定のJobに紐付かない
- キャンセルはコルーチンそのものをキャンセルする必要がある
- 別のScope内でGlobalScopeを使ってコルーチンを立ち上げてScopeをキャンセルしても立ち上げたコルーチンはキャンセルされない
- あんまり使うべきじゃない
- 公式ドキュメントにも使うのを強くおすすめしません、くいらいの温度感で書かれている
- 例外として特定のSuspending Function内で
Dispatchers.Unconfined
を使ってproduce
などのoperatorを使う時、に使うがこれもレアケースだと思う
2.Suspending Functionを書く
- 特に難しいことはなくて
suspend
キーワードを使った関数を作ればよい - 基本的にSuspending Function内は同期的に実行される
- 中で
coroutineScope
とasync
を使ってDeferred
を立ち上げたりした場合は別
- 中で
- 中の処理をまるまる別のDispatcherで処理したい場合は
withContext(defaultDispatcher) { /# do something #/ }
みたいな感じで囲ってやるとよい
3. コルーチンビルダーでコルーチンを作る
- 戻り値を受け取る必要がなければ
launch
- 値を受け取りたければ
async
- ただし
await
はSuspending FunctionなのでSuspending Function内で実行する必要がある launch
->coroutineScope
->async
みたいな囲うのが一般的- 例外を内部でcatchして値を返すのなら
coroutineScope
を使わないのもあり
- 例外を内部でcatchして値を返すのなら
- ただし
- 特定のContextで実行したければ
withContext
- 引数にCoroutineContextを受け取る
- 一般的にCoroutineDispatcherを
Dispatchers.Default
に切り替えて実行したい時に使われることが多い NonCancelable
を渡すとキャンセルされない- あまり使い道はなさそう
コルーチンを書く時に気を付けたいこと
例外処理
- 例外処理は普通に
try/catch
できる - ちゃんとcatchしないと親のコルーチンが死ぬ
SupervisorJob
を使っていると死なない- CoroutineScope単位で死ぬので
coroutineScope
で囲う死なないという話はあるが、それでもcatchしないと普通に例外が吐かれてアプリが死ぬ
キャンセル可能な作りにする
- コルーチンはキャンセルしてもコード側がキャンセル可能な作りになっていない限り処理が続行される
- キャンセル可能に作っておくとアクセスされたら困るタイミングでViewにアクセスする、といった状況が避けやすい
- よく使うキャンセル可能なコルーチンを作る関数
delay
- 処理を中断して特定の時間後に再開することで後続の処理を遅らせる、コルーチンがキャンセルされたら再開されずにキャンセルされる
yield
- その場でコルーチンを中断する、コルーチンがキャンセルされたら再開されずにキャンセルされる
- Dispatcherが解放されるので、詰まってる別のコルーチンの処理を終わらせてから続きをする、といったことにも使える
kotlinx.coroutines
にあるサスペンド関数は全てキャンセル可能になっている- https://kotlinlang.org/docs/reference/coroutines/cancellation-and-timeouts.html#cancellation-is-cooperative
All the suspending functions in
kotlinx.coroutines
are cancellable.