目次
Kotlin 1.3: コルーチンコンテキストとディスパッチャ (2/3) からの続きです。
検証環境
- Kotlin 1.3.0-rc-146
- kotlinx-coroutines-core 0.30.2
明示的なジョブを経由したキャンセル
知識をコンテキスト、子のコルーチン、ジョブに同時に適用してみましょう。 アプリケーションがあるライフサイクルをもつオブジェクト、ただしコルーチンではないもの、を有していると仮定します。 例えば Android のアプリケーションを書いていて、 いくつかのコルーチンを Android Activity のコンテキストで起動して、非同期処理としてデータの取得、更新、アニメーションなどを行う場合です。 全てのそれらのコルーチンは Activity が破棄された時には メモリリーク回避のために キャンセルされるべきです。
Activity のライフサイクルに結びつけたジョブインスタンスを作ることで コルーチンのライフサイクルを管理します。 Activity が作られるときに ジョブインスタンスが Job()
ファクトリ関数 によって作成され、 Activity が削除される時にそのジョブがキャンセルされます。 例を示します。
1 2 3 4 5 6 7 8 9 10 11 |
class Activity : CoroutineScope { lateinit var job: Job fun create() { job = Job() } fun destroy() { job.cancel() } // to be continued ... |
この例では Activity に CoroutineScope
インターフェース を実装しています。 CoroutineScope.coroutineContext
プロパティ をオーバライドするだけで、 スコープ内で起動されるコルーチンのコンテキストを指定できます。 目的のディスパッチャ、この例では Dispatchers.Default
、 とジョブを結合します。
1 2 3 4 5 |
// Activity クラスの続き override val coroutineContext: CoroutineContext get() = Dispatchers.Default + job // to be continued ... |
これで、 明示的にコンテキストを記述することなく、 コルーチンを Activity の スコープ 内 で起動できます。 それぞれ異なる時間待機する 10 の コルーチン を 起動するコード例を示します。
1 2 3 4 5 6 7 8 9 10 11 12 |
// Activity クラス の 続き fun doSomething() { // launch ten coroutines for a demo, each working for a different time repeat(10) { i -> launch { // variable delay 200ms, 400ms, ... etc delay((i + 1) * 200L) println("Coroutine $i is done") } } } } |
メインファンクション内で Activity を作成し、 テストとして作った doSomething
関数 を呼び出します。 そして、 500 ms 後 に Activity を削除します。 その時起動された全てのコルーチンが取り消され、 待っていてもスクリーンに表示されないことが確認できます。
1 2 3 4 5 6 7 8 9 10 |
fun main(args: Array<String>) = runBlocking<Unit> { val activity = Activity() activity.create() // create an activity activity.doSomething() // run test function println("Launched coroutines") delay(500L) // delay for half a second println("Destroying activity!") activity.destroy() // cancels all coroutines delay(1000) // visually confirm that they don't work } |
出力は次のようになります。
1 2 3 4 |
Launched coroutines Coroutine 0 is done Coroutine 1 is done Destroying activity! |
見ての通り、 最初の2つのコルーチンのみメッセージを表示していて、 Activity.destroy()
内 の job.cancel()
によって、 他のものがキャンセルされています。
スレッドローカルデータ
時々、スレッドローカルなデータを渡す機能が便利になることがあります。 しかし、コルーチンにおいては特定のスレッドに固定されていないので、そのようなことを多くの定型文を使わずに達成することは困難です。
ThreadLocal
, asContextElement
拡張関数 がそういったときのために存在します。 追加のコンテキスト要素を作り出し、 与えられた ThreadLocal
の値を保持し、 コルーチンがコンテキストを変更する際に毎回復元します。
サンプルコードを示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// declare thread-local variable val threadLocal = ThreadLocal<String?>() fun main(args: Array<String>) = runBlocking<Unit> { threadLocal.set("main") println("Pre-main, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'") val job = launch(Dispatchers.Default + threadLocal.asContextElement(value = "launch")) { println("Launch start, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'") yield() println("After yield, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'") } job.join() println("Post-main, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'") } |
この例では新しいコルーチンを Dispatchers.Default
を利用してバックグラウンドスレッドプールの中で起動します。 起動したコルーチンはスレッドプールとは別のスレッドで動きます。 しかしそれでも threadLocal.asContextElement(value = "launch")
で設定したスレッドローカルな値を保持しており、 それはコルーチンがどのスレッドで動いているかとは無関係です。 出力は次のようになります。
1 2 3 4 |
Pre-main, current thread: Thread[main @coroutine#1,5,main], thread local value: 'main' Launch start, current thread: Thread[DefaultDispatcher-worker-1 @coroutine#2,5,main], thread local value: 'launch' After yield, current thread: Thread[DefaultDispatcher-worker-1 @coroutine#2,5,main], thread local value: 'launch' Post-main, current thread: Thread[main @coroutine#1,5,main], thread local value: 'main' |
ThreadLocal
はファーストクラスのサポートを持っており、 kotlinx.coroutines
が提供するプリミティブと一緒に使用できます。 1つ重要な制限があり、スレッドローカルが変更された場合、新しい値はコルーチンの呼び出し元に伝播されず (コンテキスト要素はすべてのThreadLocal
オブジェクトへのアクセスを追跡できないため)、 更新された値は次の一時停止時に失われます。 コルーチンのスレッドローカルの値を更新するにはwithContext
を使用します。 詳しくは、asContextElement
のドキュメントに記載があります。
あるいは、値を class Counter(var i:Int)
のような変更可能なボックスに格納することができ、 その場合、スレッドローカルな変数に格納されます。 ただし、このケースでは、開発者がその変更可能なボックス内の変数に潜在的に同時発生する変更を同期する必要があります。
たとえば、MDC、トランザクションコンテキスト、またはデータを渡すためにスレッドローカルを内部的に使用する他のライブラリとの統合などの高度な使い方については、 実装する必要がある ThreadContextElement
インターフェース のドキュメントに記載があります。