LoginSignup
0
1

More than 1 year has passed since last update.

【Coroutinesガイド】コルーチンコンテキストとディスパッチャー

Posted at

※ソース記事はこちら
コルーチンはKotlin標準ライブラリで定義されているCoroutineContext型の値によって表されるいくつかのコンテキスト内で常に実行する。
コルーチンコンテキストは、様々な要素のセットである。主な要素は、以前に見てきたコルーチンのJobと、このセクションでカバーするディスパッチャーである。

ディスパッチャーとスレッド

コルーチンコンテキストには、対応するコルーチンが実行するために使う、スレッドあるいはスレッド群を決定するコルーチンディスパッチャー(CoroutineDispatcherを参照)が含まれている。コルーチンディスパッチャーはコルーチンの実行を特定のスレッドに制限したり、スレッドプールにディスパッチしたり、制限せずに実行させることができる。
launchasyncのようなすべてのコルーチンビルダーは、オプションのCoroutineContextパラメータを持ち、新しいコルーチンと他のコンテキスト要素のためにディスパッチャーを明示して使うことができる。
次の例を試してみよう。

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    launch { // 親のコンテキスト、main runBlockingコルーチン
        println("main runBlocking      : I'm working in thread ${Thread.currentThread().name}")
    }
    launch(Dispatchers.Unconfined) { // 制限なし -- メインスレッドで動作する
        println("Unconfined            : I'm working in thread ${Thread.currentThread().name}")
    }
    launch(Dispatchers.Default) { // DefaultDispatcherにディスパッチされる
        println("Default               : I'm working in thread ${Thread.currentThread().name}")
    }
    launch(newSingleThreadContext("MyOwnThread")) { // 自身の新スレッドを得る
        println("newSingleThreadContext: I'm working in thread ${Thread.currentThread().name}")
    }    
}

これは次の出力を生成する。(異なる順序かもしれない)

Unconfined            : I'm working in thread main
Default               : I'm working in thread DefaultDispatcher-worker-1
newSingleThreadContext: I'm working in thread MyOwnThread
main runBlocking      : I'm working in thread main

launch { ... }は、パラメータ無しで使うとき、起動元であるCoroutineScopeからコンテキスト(したがってディスパッチャも)を継承する。このケースでは、mainスレッドで動作するメインのrunBlockingコルーチンのコンテキストを継承している。
Dispatchers.Unconfinedも、mainスレッドで実行するように見えるが、実際は後で説明する別のメカニズムで動作する特別なディスパッチャである。
デフォルトディスパッチャーは、他のディスパッチャーがスコープで明示的に指定されてないときに使われる。それはDispatchers.Defaultで表され、共有のバックグラウンドスレッドプールを使用する。
newSingleThreadContextは、実行するコルーチンのためにスレッドをつくる。専用のスレッドは大変高価なリソースである。実際のアプリケーションでは、それは不要になったらclose関数を使ってリリースされなければならない。あるいは、トップレベル変数に保存して、アプリケーション中で再利用する。

制限無しvs制限付きディスパッチャー

Dispatchers.Unconfinedのコルーチンディスパッチャーは、呼び出しスレッドでコルーチンを監視するが、それは最初の一時停止ポイントまでである。一時停止の後は、呼び出されたsuspend関数によって完全に決められたスレッドで復帰する。制限無しディスパッチャーは、CPUを消費せず、特定のスレッドで制限される共有データ(UIのような)を更新しないコルーチンに適している。
一方で、ディスパッチャーはデフォルトで外側のCoroutineScopeから継承する。特にrunBlockingコルーチンのためのデフォルトディスパッチャーは、呼び出しスレッドに制限されており、それを継承することは、予測可能なFIFOスケジュールを持つこのスレッドに対する制限された実行の効果を持つ。

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    launch(Dispatchers.Unconfined) { // 制限なし -- メインスレッドで動く
        println("Unconfined      : I'm working in thread ${Thread.currentThread().name}")
        delay(500)
        println("Unconfined      : After delay in thread ${Thread.currentThread().name}")
    }
    launch { // 親のコンテキスト、メインのrunBlockingコルーチン
        println("main runBlocking: I'm working in thread ${Thread.currentThread().name}")
        delay(1000)
        println("main runBlocking: After delay in thread ${Thread.currentThread().name}")
    }    
}

次の出力を生成する。

Unconfined      : I'm working in thread main
main runBlocking: I'm working in thread main
Unconfined      : After delay in thread kotlinx.coroutines.DefaultExecutor
main runBlocking: After delay in thread main

このように、runBlocking {...}から継承したコンテキストを持つコルーチンは、mainスレッドで実行を続けるが、一方、制限無しのものはdelay関数が使っているデフォルトの実行スレッドで復旧する。

制限無しのディスパッチャーは、コルーチン内のいくつかの操作がすぐに行われなければならないため、実行のためのコルーチンにディスパッチすることがのちに不要にであるか、望ましくない副作用を生むような、まれなケースで役に立つことがありうる。一般には制限無しのディスパッチャーは使うべきではない。

コルーチンとスレッドのデバッグ

コルーチンは、あるスレッドdで一時停止し、別のスレッドで再開することがある。シングルスレッドのディスパッチャーでさえ、コルーチンがどこでいつ何をしているのか、特別なツールが無いと、理解することが難しいかもしれない。

IDEAを用いたデバッグ

Kotlinプラグインのコルーチンデバッガーは、IntelliJ IDEAにおいてコルーチンのデバッグを簡単にしてくれる。

デバッグはkotlinx-coroutines-coreのバージョン1.3.8以降で動作する。

デバッグツールウィンドウにはコルーチンタブがある。このタブでは、現在実行中と一時停止中の両方のコルーチンについての情報を見つけることができる。コルーチンは実行しているディスパッチャーでグルーピングされている。
scr1
コルーチンデバッガーで次のことができる。

  • それぞれのコルーチンの状態の確認
  • 現在実行中と一時停止中の両方のコルーチンのローカル変数とキャプチャされた変数の値の参照
  • コルーチン内部のコールスタックと同様に完全なコルーチンの生成スタックの参照。スタックには変数値を持つすべてのフレームが含まれ、標準のデバッグの間に失われてしまうものも含まれている。
  • それぞれのコルーチンの状態とそのスタックを含む完全なレポート。取得するには、コルーチンタブの内部を右クリックして、その後、コルーチンダンプの取得をクリックする。

コルーチンのデバッグを開始するには、ブレークポイントをセットし、デバッグモードでアプリケーションを実行するだけである。
チュートリアルでコルーチンのデバッグについての知識を深める。

ログを用いたデバッグ

コルーチンデバッガー抜きでスレッドを用いたアプリケーションをデバッグする、別のアプローチは、それぞれのログステートメントでログファイルにスレッド名を出力することである。この機能はロギングフレームワークで広くサポートされている。コルーチンを使うとき、スレッド名だけではコンテキストの多くが提供されないので、kotlinx.coroutinesにはそれを簡単にするためのデバッグ機能が含まれている。
次のコードを-Dkotlinx.coroutines.debugJVMオプションをつけて実行する。

import kotlinx.coroutines.*

fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")

fun main() = runBlocking<Unit> {
    val a = async {
        log("I'm computing a piece of the answer")
        6
    }
    val b = async {
        log("I'm computing another piece of the answer")
        7
    }
    log("The answer is ${a.await() * b.await()}")    
}

3つのコルーチンがある。メインコルーチン(#1)はrunBlockingの内部であり、二つのコルーチンはdeferred値のa(#2)とb(#3)を計算している。それらはすべてrunBlockingのコンテキストで実行され、メインスレッドで制限されている。このコードの出力は以下のとおり。

[main @coroutine#2] I'm computing a piece of the answer
[main @coroutine#3] I'm computing another piece of the answer
[main @coroutine#1] The answer is 42

ログ関数はスレッド名を大括弧内に出力し、現在実行しているコルーチンに追加された識別子を持つmainスレッドが確認できる。この識別子はデバッグモードがオンの間は、結果的にすべての生成されたコルーチンに割り当てられる。

デバッグモードはJVMが-eaオプションをつけて実行するときもオンになる。DEBUG_PROPERTY_NAMEプロパティのドキュメント内で、デバッグ機能について続きを読むことができる。

スレッド間のジャンプ

次のコードを-Dkotlinx.coroutines.debugJVMオプションつきで実行する。(debugを参照)

import kotlinx.coroutines.*

fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")

fun main() {
    newSingleThreadContext("Ctx1").use { ctx1 ->
        newSingleThreadContext("Ctx2").use { ctx2 ->
            runBlocking(ctx1) {
                log("Started in ctx1")
                withContext(ctx2) {
                    log("Working in ctx2")
                }
                log("Back to ctx1")
            }
        }
    }    
}

いくつかの新しいテクニックを示している。一つは明示したコンテキストを持つrunBlockingを使うもので、もうひとつは、withContextを使い、まだ同じコルーチンにとどまっている間にコルーチンのコンテキストを変更するものである。
以下の出力を見ることができる。

[Ctx1 @coroutine#1] Started in ctx1
[Ctx2 @coroutine#1] Working in ctx2
[Ctx1 @coroutine#1] Back to ctx1

留意すべきは、この例ではもう不要であるときに、newSingleThreadContextで作られたスレッドを解放するため、Kotlin標準ライブラリからuse関数を使っている。

コンテキストのジョブ

コルーチンのJobは、コンテキストの一部であり、coroutineContext[Job]式を使うことで取り出すことができる。

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    println("My job is ${coroutineContext[Job]}")    
}

デバッグモードでは、このような出力になる。
My job is "coroutine#1":BlockingCoroutine{Active}@6d311334
留意すべきは、CoroutineScopeisActiveは、単にcoroutineContext[Job]?.isActive == trueの便利なショートカットである。

コルーチンの子孫

コルーチンが他のコルーチンのCoroutineScopeから起動すると、それはCoroutineScope.coroutineContext経由でコンテキストを継承し、新しいコルーチンのJobは、親のコルーチンのjobの子になる。親のコルーチンがキャンセルされると、そのすべての子も再帰的にキャンセルされる。
しかし、この親子関係は、次の二つの方法のいずれかで明示的にオーバーライドできる。

  1. コルーチンを起動するときに異なるスコープ(例としてGlobalScope.launch)が明示的に指定された場合、それは親のスコープからJobを継承しない。
  2. 異なるJobオブジェクトが新しいコルーチンのためのコンテキストとして渡されるとき(以下の例でお見せするように)、親のスコープのJobを上書きする。

両方のケースにおいても、起動したコルーチンは、起動元のスコープとは結びついておらず、独立して働く。

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    // ある種の要求を処理するコルーチンを起動する
    val request = launch {
        // 他の二つのJobを生成する
        launch(Job()) { 
            println("job1: I run in my own Job and execute independently!")
            delay(1000)
            println("job1: I am not affected by cancellation of the request")
        }
        // そして別のものは親のコンテキストを継承する
        launch {
            delay(100)
            println("job2: I am a child of the request coroutine")
            delay(1000)
            println("job2: I will not execute this line if my parent request is cancelled")
        }
    }
    delay(500)
    request.cancel() // 要求の処理をキャンセルする
    println("main: Who has survived request cancellation?")
    delay(1000) // 何が起こるか見るために1秒間メインスレッドを遅延させる
}

このコードの出力は次のようになる。

job1: I run in my own Job and execute independently!
job2: I am a child of the request coroutine
main: Who has survived request cancellation?
job1: I am not affected by cancellation of the request

親の責任

親のコルーチンは常にすべての子の完了を待つ。親は起動したすべての子を明示的に追跡する必要はなく、最後にそれらを待つためにJob.joinを使う必要はない。

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    // ある種の要求を処理するコルーチンを起動する
    val request = launch {
        repeat(3) { i -> // いくつかの子のジョブを起動する
            launch  {
                delay((i + 1) * 200L) // 遅延の変数 200ms, 400ms, 600ms
                println("Coroutine $i is done")
            }
        }
        println("request: I'm done and I don't explicitly join my children that are still active")
    }
    request.join() // すべての子を含む要求の完了を待つ
    println("Now processing of the request is complete")
}

結果は次のようになる。

request: I'm done and I don't explicitly join my children that are still active
Coroutine 0 is done
Coroutine 1 is done
Coroutine 2 is done
Now processing of the request is complete

デバッグのためのコルーチンのネーミング

自動的に割り当てられたIDはコルーチンが度々ログ出力をし、同じコルーチンから来るログ記録を関連付ける必要があるだけのときは良い。しかし、コルーチンがと特定の要求の処理や、いくつかの特定のバックグラウンドタスクの実行と結びついている場合、デバッグ目的のために、明示的に命名する方が良い。CoroutineNameコンテキスト要素は、スレッド名と同じ目的のために役に立つ。デバッグモードがONのとき、それにはそのコルーチンを実行しているスレッド名が含まれる。
次の例は、この概念を示している。

import kotlinx.coroutines.*

fun log(msg: String) = println("[${Thread.currentThread().name}] $msg")

fun main() = runBlocking(CoroutineName("main")) {
    log("Started main coroutine")
    // 二つのバックグラウンドでの値の計算を実行する。
    val v1 = async(CoroutineName("v1coroutine")) {
        delay(500)
        log("Computing v1")
        252
    }
    val v2 = async(CoroutineName("v2coroutine")) {
        delay(1000)
        log("Computing v2")
        6
    }
    log("The answer for v1 / v2 = ${v1.await() / v2.await()}")    
}

-Dkotlinx.coroutines.debugJVMオプションをつけて生成された出力は、次のものと同様である。

[main @main#1] Started main coroutine
[main @v1coroutine#2] Computing v1
[main @v2coroutine#3] Computing v2
[main @main#1] The answer for v1 / v2 = 42

コンテキスト要素の結合

時々、コルーチンコンテキストに対して、複数の要素を定義する必要がある。そのために+演算子を使うことができる。例えば、明示的に特定のディスパッチャーと、明示的に特定の名前が同時についた、コルーチンを起動することができる。

import kotlinx.coroutines.*

fun main() = runBlocking<Unit> {
    launch(Dispatchers.Default + CoroutineName("test")) {
        println("I'm working in thread ${Thread.currentThread().name}")
    }    
}

-Dkotlinx.coroutines.debugJVMオプションをつけた、このコードの出力は次のようになる。

I'm working in thread DefaultDispatcher-worker-1 @test#2

コルーチンのスコープ

コンテキスト、子とジョブについて一緒に知識を活用しよう。ライフサイクルを持つオブジェクトがあるが、オブジェクトはコルーチンではないアプリケーションを仮定する。例えば、我々はAndroidアプリケーションを書いており、Android Activityのコンテキストの中で様々なコルーチンを起動し、データを取得、更新、アニメーション等々のために非同期の操作を実行する。これらすべてのコルーチンは、Activityがメモリリークを防ぐために破壊されるとき、キャンセルされなければならない。もちろん、コンテキストとジョブを操作して、Activityのライフサイクルとコルーチンを手動で結びつけることはできるが、kotlinx.coroutinesにはそれを抽象的なカプセル化であるCoroutineScopeが提供されている。すべてのコルーチンビルダーは、それの拡張で制限されているため、すでにコルーチンスコープについては精通しているはずである。
Activityのライフサイクルに結びついたCoroutineScopeのインスタンスを作ることにより、コルーチンのライフサイクルを管理する。CoroutineScopeインスタンスはCoroutineScope()か、MainScope()ファクトリ関数により作ることができる。前者は一般的な目的のスコープを作り、一方、後者はUIアプリケーション用のスコープを作り、デフォルトのディスパッチャーとしてDispatchers.Mainを使う。

class Activity {
    private val mainScope = MainScope()

    fun destroy() {
        mainScope.cancel()
    }
    // 続く

これで定義されたscopeを使い、このActivityのスコープ内でコルーチンを起動することができる。デモののため、バラバラの時間遅延する10個のコルーチンを起動する。

// Activityクラスの続き
    fun doSomething() {
        // デモのため10のコルーチンを起動し、それぞれ異なる時間動作する
        repeat(10) { i ->
            mainScope.launch {
                delay((i + 1) * 200L) // 遅延変数 200ms, 400ms, ... など
                println("Coroutine $i is done")
            }
        }
    }
} // Activityクラスの終了

Activityを作成するmain関数では、テストのdoSomething関数を呼び、500ms後にActivityを破壊する。これはdoSomethingから起動されたすべてのコルーチンをキャンセルする。Activityの破壊の後では、たとえ少し長く待ったとしても、もうメッセージが出力されないことからわかる。

fun main() = runBlocking<Unit> {
    val activity = Activity()
    activity.doSomething() // テスト関数を実行
    println("Launched coroutines")
    delay(500L) // 500msの間遅延
    println("Destroying activity!")
    activity.destroy() // すべてのコルーチンをキャンセルする
    delay(1000) // それらが動かないことを視覚的に確認する。    
}

この例の出力は以下のようになる。

Launched coroutines
Coroutine 0 is done
Coroutine 1 is done
Destroying activity!

ご覧のとおり、最初の二つのコルーチンがメッセージを出力するだけで、他はActivity.destroy()での単一のjob.cancelの呼び出しによってキャンセルされる。

留意すべきは、Androidはライフサイクルを持つすべての実体で、コルーチンスコープの提供者サポートを持つ。対応するドキュメントを参照のこと。

スレッドローカルデータ

時々、いくつかのスレッドローカルなデータをコルーチンに対して、あるいはコルーチン間で送る能力があると便利である。しかしそれらは特定のスレッドに縛られていないので、もし手動で行うとボイラープレートコードに至りそうである。
[ThreadLocal](https://docs.oracle.com/javase/8/docs/api/java/lang/ThreadLocal.html)のため、asContextElement拡張関数が、助けのために存在する。それは追加のコンテキスト要素を作り、与えられたThreadLocalの値を保持し、コルーチンがコンテキストを切り替えるときはいつでも復元する。
動かして実演するのは簡単である。

import kotlinx.coroutines.*

val threadLocal = ThreadLocal<String?>() //thread-local変数の宣言

fun main() = 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")を使って指定されたスレッドローカル変数の値は、コルーチンがどんなスレッドで実行しても。持ったままである。したがって出力は(debugで)次のようになる。

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-2 @coroutine#2,5,main], thread local value: 'launch'
Post-main, current thread: Thread[main @coroutine#1,5,main], thread local value: 'main'

対応するコンテキスト要素をセットすることは忘れやすい。コルーチンを実行しているスレッドが異なる場合、コルーチンからアクセスされるスレッドローカル変数は、そのとき予期せぬ値を持っているかもしれない。そのような状況を避けるため、ensurePresetメソッドを使い、不適切な使用の場合は早期に失敗すさせることが推奨されている。
ThreadLocalは最上級のサポートを持ち、kotlinx.coroutinesが提供するどんなプリミティブ値も使うことができる。ただし一つ制限がある。スレッドローカルが変更されると、新しい値はコルーチンの呼び出し元には伝播せず(なぜならコンテキスト要素はすべてのThreadLocal値へのアクセスを追跡できないため)、変更された値は次の一時停止で失われる。コルーチンでスレッドローカルの値を更新するには、withContextを使い、詳細はasContextElementを参照のこと。
あるいは、class Counter(var i: Int)のように値を変更可能な箱に格納し、それにスレッドローカルな値を格納することもできる。しかしこのケースでは、この変更可能な箱の値に対する潜在的な同時変更を同期することに完全に責任を持たなければならない。
さらに高度な使い方、例えばログのMDCの統合や、トランザクションコンテキストや、値を渡すためにスレッドローカルを内部的に使う他のライブラリの例のために、実装すべきThreadContextElementインターフェイスの文書を参照のこと。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1