目次
Kotlin 1.3: コルーチンコンテキストとディスパッチャ (1/3)の続きです。
検証環境
- Kotlin 1.3.0-rc-146
- kotlinx-coroutines-core 0.30.2
スレッド間ジャンプ
次のコードを JVM オプション -Dkotlinx.coroutines.debug
をつけて実行してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
fun log(msg: String) = println("[${Thread.currentThread().name}] $msg") fun main(args: Array<String>) { 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
関数 を使って コルーチンのコンテキストを変更しています。 出力を見るとそれがわかります。
1 2 3 |
[Ctx1 @coroutine#1] Started in ctx1 [Ctx2 @coroutine#1] Working in ctx2 [Ctx1 @coroutine#1] Back to ctx1 |
このサンプルコードでは、 Kotlin 標準関数 の user
を使っています。Note, that this example also uses use function from the Kotlin standard library to release threads that are created with newSingleThreadContext when they are no longer needed
コンテキスト内のジョブ
コルーチンのジョブはそのコンテキストの一部です。 コルーチン内では coroutineContext[Job]
式 を使って コンテキストを取得することができます。 次に例を示します。
1 2 3 |
fun main(args: Array<String>) = runBlocking<Unit> { println("My job is ${coroutineContext[Job]}") } |
出力は次のようになります。
1 |
My job is "coroutine#1":BlockingCoroutine{Active}@3ecd23d9 |
CoroutimeScope
の isActive
プロパティ は、 実質的に croutineContext[Job]
の isActive
プロパティ へのエイリアスになっています。
1 2 |
public val isActive: Boolean get() = coroutineContext[Job]?.isActive ?: true |
コルーチンの子ども
コルーチン が 別のコルーチンの CoroutineScope
で起動された場合、 起動されたコルーチンは CoroutineScope.coroutineContext
を経由してコンテキストを継承し、 新しいコルーチンのジョブを親のコルーチンのジョブとして追加します。 親のコルーチンがキャンセルされたら、子のコルーチンも再帰的にキャンセルされます。
GlobalScope
がコルーチンの起動に使われた場合、 起動されたところのスコープには追加されず、 独立して実行されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
fun main(args: Array<String>) = runBlocking<Unit> { // launch a coroutine to process some kind of incoming request val request = launch { // it spawns two other jobs, one with GlobalScope GlobalScope.launch { println("job1: I run in GlobalScope and execute independently!") delay(1000) println("job1: I am not affected by cancellation of the request") } // and the other inherits the parent context 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") } } println("# wait 500 ms.") delay(500) println("# cancel request.") // cancel processing of the request request.cancel() println("# wait 1000 ms.") // delay a second to see what happens delay(1000) println("main: Who has survived request cancellation?") } |
アウトプットは次のようになります。
1 2 3 4 5 6 7 |
# wait 500 ms. job1: I run in GlobalScope and execute independently! job2: I am a child of the request coroutine # cancel request. # wait 1000 ms. job1: I am not affected by cancellation of the request main: Who has survived request cancellation? |
実行開始から 約 1500 ms 待った時の出力です。 (“約”と書いているのは println
の処理時間があるからです。) ジョブがキャンセルされていなければ、 1100 ms ほど待ったところで、 “job2” の出力が出るはずです。 子のコルーチンとして再帰的にキャンセルされているので出力されていません。
親の責任
親のコルーチンは常に全ての子のコルーチンの完了を待ちます。 親のコルーチンでは、 明示的に全ての子のコルーチンをトラッキングさせる必要はなく、 Job.join
を実行する必要もありません。 次にサンプルコードを示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
fun main(args: Array<String>) = runBlocking<Unit> { // launch a coroutine to process some kind of incoming request val request = launch { // launch a few children jobs repeat(3) { i -> launch { // variable delay 200ms, 400ms, 600ms delay((i + 1) * 200L) println("Coroutine $i is done") } } println("request: I'm done and I don't explicitly join my children that are still active") } request.join() // wait for completion of the request, including all its children println("Now processing of the request is complete") } |
出力結果は次のようになります。
1 2 3 4 5 |
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 |
確かに 最初の launch
内 で作られた子のコルーチンは全て、 親の処理が先に終わっても、実行されています。 例えば 下から 3行目にある join
の行をコメントアウトして実行すると次のような出力になります。
1 2 3 4 5 |
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 |
join
を実行していないので、 runBlocking
内 の println
が最初に実行されていますが、 runBlocking
内 で起動されたコルーチンは全て最後まで実行されています。 (Kotlin 1.3: Coroutine の基本 (1 of 2) で見た通りですね。)
デバッグのためにコルーチンに名前をつける
自動的に付与された ID は コルーチンがログを頻繁に出力し、 それをコルーチンごとにまとめるだけであれば有効です。 しかし、 コルーチンが特定のリクエストまたは特定のバックグラウンドタスクと結びついている場合には、 デバッグのために明示的に命名するのが適しています。 CoroutineName
コンテキスト は スレッド名と同じ機能を有しています。 デバッグモードが有効である場合に、 そのコルーチンが実行されているスレッド名の中に表示されます。
以下がサンプルコードです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
fun log(msg: String) = println("[${Thread.currentThread().name}] $msg") fun main(args: Array<String>) = runBlocking(CoroutineName("main")) { log("Started main coroutine") // run two background value computations 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()}") } |
出力は次のようになります。
1 2 3 4 |
[main @main#1] Started main coroutine [main] Computing v1 [main] Computing v2 [main @main#1] The answer for v1 / v2 = 42 |
ドキュメントによれば、次のようになるはずなのですが、そうなりませんでした。
1 2 3 4 |
[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 |
次のようにコードを変更すると、指定した名称が出力されるようになりました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
suspend fun log(msg: String) { coroutineScope { println( "[${Thread.currentThread().name} ${coroutineContext[Job]}] $msg") } } fun main(args: Array<String>) = runBlocking(CoroutineName("main")) { log("Started main coroutine") // run two background value computations 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()}") } |
出力は次のようになりました。
1 2 3 4 |
[main @main#1 "main#1":ScopeCoroutine{Active}@75881071] Started main coroutine [main "v1coroutine#2":ScopeCoroutine{Active}@3d0f8e03] Computing v1 [main "v2coroutine#3":ScopeCoroutine{Active}@5f71c76a] Computing v2 [main @main#1 "main#1":ScopeCoroutine{Active}@1d7acb34] The answer for v1 / v2 = 42 |
コンテキストを結びつける
時には複数の要素をコルーチンのコンテキストに指定する必要があります。 それは +
演算子を使うことで可能になります。 例として、 コルーチンを明示的に指定されたディスパッチャで起動し、明示的に名前を指定する場合のコードを示します。
1 2 3 4 5 |
fun main(args: Array<String>) = runBlocking<Unit> { launch(Dispatchers.Default + CoroutineName("test")) { println("I'm working in thread ${Thread.currentThread().name}") } } |
デバッグモードで実行すると次のような出力が得られます。
1 |
I'm working in thread DefaultDispatcher-worker-1 @test#2 |
Kotlin 1.3: コルーチンコンテキストとディスパッチャ (3/3) に続きます。