Coroutineはいくつか分かりにくいものがありますが、その中でも個人的に CoroutineContext
については特に分かりにくいように感じます。
CoroutineContext
について理解しておくと、Coroutineのキャンセルや例外をどう扱えば良いかが分かってきます。
色々な要素が絡んでるので説明する順番が難しくて分かりにくい箇所があるかもしれませんが、自分で試したりすると理解しやすいかと思います。
CoroutineContextとは?
CoroutineContext
はCoroutineをどのように動作させるかを定義するものになります。Coroutineが実行には必ず CoroutineContext
が必要になります。
CoroutineContext
は一つの何かを指すのではなく、様々な要素のセットになっています。
CoroutineContextを指定する
CoroutineContext
を指定するにはいくつか方法があります。以下にいくつか載せておきます。
CoroutineContextとCoroutineScope
Coroutineは CoroutineScope
上で動くものになります。そして、この CoroutineScope
は CoroutineContext
を持っています。
そして、Coroutineを作るたびに CoroutineScope
は作られます。
イメージとしてはこんな感じです。
launch
するたびに新しく CoroutineScope
が作られ、その CoroutineScope
はそれぞれ CoroutineContext
を持っています。
Coroutineの親子関係については後述します。
CoroutineContextの要素
CoroutineContext
は代表的なものとしては以下のようなものがあります。
- Job
- CoroutineDispatcher
- CoroutineName
- CoroutineExceptionHandler
基本はこの4つがメインになってくると思います。これらは CoroutineContext.Element
を実装しています。CoroutineContext.Element
は自分で作ることも可能だったりします。
これらの代表的な要素について説明していきます。
Job
Job
はキャンセルを扱うことができ、ライフサイクルを持っています。
( Job
のライフサイクルについては、またどこか別の記事で紹介したいと思います)
Job
にて親子関係を制御することができます。
Coroutineが作られると CoroutineContext
に新しく Job
が割り当てられるようになっています。
詳しくは後述します。
CoroutineName
Coroutine名を指定することができます。スレッド名を表示すると指定した CoroutineName
が表示されます。これはデバッグモードのみになります。
これを -Dkotlinx.coroutines.debug
のJVMオプションをつけて実行すると以下のように出力されます。 @
マーク以降が指定した名前になってい
CoroutineName = DefaultDispatcher-worker-1 @Sample#1
IntelliJ IDEAを使うと確認しやすいです。
あまり使うことは無さそうです。
※追記
最初はAndroidではできないと書きましたが、Applicationクラス等で System.setProperty(DEBUG_PROPERTY_NAME, DEBUG_PROPERTY_VALUE_ON)
で可能です。
CoroutineDispatcher
Dispatcher.Default
や Dispatchers.IO
があり、Coroutineを動かすスレッドを指定することができます。
CoroutineDispatcher
が指定されなかったときは、Dispatcher.Default
が使われることになります。 launch
実行時に CoroutineContext
に Dispatcherが指定されていないときは自動で追加されます。
Dispatchers.Unconfined
というのもありますが、こちらは基本的に使わないようにしてください。これはスレッドを指定するものではなくCoroutineが実行したときのスレッドをそのまま使います。更に途中で withContext
でDispatcherが切り替えられると、それ以降の処理がそれに影響されてしまいます。
CoroutineExceptionHandler
Coroutineの処理でキャッチできなかった例外を処理することができます。
CoroutineContextの合成
CoroutineContext
は複数の要素があると説明しましたが、これらを組み合わせるには +
で足していくだけです。
この例では Job
と CoroutineName
と Dispatcher
を持った CoroutineScope
を作っています。
CoroutineContext.Keyについて
CoroutineContext
を組み合わせる際に同じ種類の ContextContext
は必ず1つになるようになっています。
例えば、 Dispatchers.IO + Dispatchers.Main
と書いた場合は同じ種類のものになるので、後に設定したものが有効です。
同じ種類かどうかを判定してるのが CoroutineContext.Key
になります。これが同じものは1つしか設定できないようになっています。
Dispatchers
の場合は、 ContinuationInterceptor
というのがKeyになっています。Job
の 場合は Job
です。
更にこの CoroutineContext.Key
を使って CoroutineContext
の要素を取得することもできます。
CoroutineContextの継承
Coroutineが作られると新しく CoroutineScope
が作られますが、その際に元なる CoroutineScope
が持っている CoroutineContext
の要素を引き継ぐようになっています。
新しくCoroutineを作る際に CoroutineContext
を変更することも可能になっています。同一の CoroutineContext.Key
のものを指定することでCoroutineContext
をマージするイメージです。
この例では、 launch
に Dispatchers.IO
を渡して、Dispatchers.Default
から変更しています。
Jobとキャンセル
Job
はキャンセルに対応するにために重要なものになります。CoroutineContext
に Job
がないとキャンセルすることが出来ません。
CoroutineScope(Dispatchers.Default)
のように CoroutineScope
を作る場合、CoroutineContext
に Job
が渡されてない場合は自動で Job
を作って CoroutineContext
に含めるようになっています。
例えば、以下のように CoroutineScope
継承した場合は CoroutineContext
に Job
は含まれません。
Job
が含まれない CoroutineScope
をキャンセルしようとするとIllegalStateExceptionが発生します。
これを回避するには Job
のインスタンスを CoroutineContext
に渡すようにします。
また、よく見かける書き方としては以下のようなものがありますが、これは Job
をキャンセルしてますが、 CoroutineScope
をキャンセルしても同じ結果になります。
CoroutineScope.cacnel()
は以下のような実装になっていて、 CoroutineContext
から Job
を取り出して、その Job
をキャンセルしています。
Jobと親子関係
CoroutineScope
から作られるCoroutineの Job
は、その CoroutineScope
が持っている Job
が親となり、作られたCoroutineの Job
はその子となります。
この親となる CoroutineScope
をキャンセルすると同時に子も孫もキャンセルされるような仕組みなっています。
親子関係になるには、 Job
が重要で以下のように、途中の lauch
で新しい Job
を渡すと、それは親子関係から外れます。
CoroutineContext
においては Job
が非常に重要で、もし間違えてしまうとCoroutineがうまくキャンセルされなくなったりします。
独自のCoroutineContextの要素を作る
普段実装してるときはほぼ作る必要はないのですが、こういうこともできるという紹介としてオマケで書いておきます。
CoroutineContext.Element
を実装することで可能になります。更にKeyを使って CoroutineContext
から取得することも可能です。
まとめ
CoroutineContextは理解しにくいものですが、ある程度考え方を抑えておけば、予期しないミスなども防げると思います。
今回はまだExceptionやSupervisorJobなんかについて触れてません。このあたりも別の機会にまとめていきたいと思っています。また、Jobのライフサイクルなどもあるので、こちらも別の機会で。