詳細CoroutineContext

Kenji Abe
9 min readJul 22, 2020

Coroutineはいくつか分かりにくいものがありますが、その中でも個人的に CoroutineContext については特に分かりにくいように感じます。

CoroutineContext について理解しておくと、Coroutineのキャンセルや例外をどう扱えば良いかが分かってきます。

色々な要素が絡んでるので説明する順番が難しくて分かりにくい箇所があるかもしれませんが、自分で試したりすると理解しやすいかと思います。

CoroutineContextとは?

CoroutineContext はCoroutineをどのように動作させるかを定義するものになります。Coroutineが実行には必ず CoroutineContext が必要になります。

CoroutineContext は一つの何かを指すのではなく、様々な要素のセットになっています。

CoroutineContextを指定する

CoroutineContext を指定するにはいくつか方法があります。以下にいくつか載せておきます。

CoroutineContextとCoroutineScope

Coroutineは CoroutineScope 上で動くものになります。そして、この CoroutineScopeCoroutineContext を持っています。

そして、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.DefaultDispatchers.IO があり、Coroutineを動かすスレッドを指定することができます。

CoroutineDispatcher が指定されなかったときは、Dispatcher.Default が使われることになります。 launch 実行時に CoroutineContext に Dispatcherが指定されていないときは自動で追加されます。

Dispatchers.Unconfined というのもありますが、こちらは基本的に使わないようにしてください。これはスレッドを指定するものではなくCoroutineが実行したときのスレッドをそのまま使います。更に途中で withContext でDispatcherが切り替えられると、それ以降の処理がそれに影響されてしまいます。

CoroutineExceptionHandler

Coroutineの処理でキャッチできなかった例外を処理することができます。

CoroutineContextの合成

CoroutineContext は複数の要素があると説明しましたが、これらを組み合わせるには + で足していくだけです。

この例では JobCoroutineNameDispatcher を持った 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 をマージするイメージです。

この例では、 launchDispatchers.IO を渡して、Dispatchers.Default から変更しています。

Jobとキャンセル

Job はキャンセルに対応するにために重要なものになります。CoroutineContextJob がないとキャンセルすることが出来ません。

CoroutineScope(Dispatchers.Default) のように CoroutineScope を作る場合、CoroutineContextJob が渡されてない場合は自動で Job を作って CoroutineContext に含めるようになっています。

例えば、以下のように CoroutineScope 継承した場合は CoroutineContextJob は含まれません。

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のライフサイクルなどもあるので、こちらも別の機会で。

参考

--

--

Kenji Abe

Programmer / Gamer / Google Developers Expert for Android, Kotlin / @STAR_ZERO