web-dev-qa-db-ja.com

Kotlinコルーチンは、ネットワーク呼び出しを行うときにいつ譲歩するかをどのようにして知るのですか?

私はKotlinコルーチンを初めて使用しました。私が理解できなかったのは、コルーチンがネットワーク呼び出しを行うときに他の人に譲るタイミングをどのようにして知るかです。

私が正しく理解していれば、コルーチンはプリエンプティブに機能します。つまり、実行するいくつかの時間のかかるタスク(通常はI/O操作)があるときに、コルーチンは他のコルーチンにいつ譲るかを知っています。

たとえば、リモートサーバーからのデータを表示するいくつかのUIをペイントしたいとし、コルーチンをスケジュールするスレッドは1つだけです。 1つのコルーチンを起動してREST APIを呼び出してデータを取得し、別のコルーチンを使用して、データに依存しない残りのUIをペイントすることができます。ただし、スレッドが1つしかないため、 、一度に実行できるコルーチンは1つだけです。データのフェッチに使用されるコルーチンが、データの到着を待っている間に先制的に生成されない限り、2つのコルーチンは順次実行されます。

私の知る限り、Kotlinのコルーチン実装は、既存のJVM実装やJDKネットワークライブラリにパッチを適用していません。したがって、コルーチンがREST APIを呼び出している場合、これはJavaスレッドを使用して行われるのと同じようにブロックする必要があります。これは、 pythonはグリーンスレッドと呼ばれます)で同様の概念のように見えます。Pythonの組み込みネットワークライブラリで機能するためには、まずネットワークライブラリに「モンキーパッチ」する必要があります。そしてネットワークライブラリ自体だけがいつ譲歩するかを知っているので、私にとってこれは理にかなっています。

だから誰もブロッキングJavaネットワークAPIを呼び出すときに譲歩するタイミングをKotlinコルーチンが知っている方法を説明できますか?そうでない場合、それは上記の例で述べたタスクを同時に実行できなかったことを意味しますシングルスレッド?

ありがとう!

12
Rafoul

コルーチンは先制的に機能します

いいえ。コルーチンでは、明示的なメソッド呼び出しを使用してコルーチンを一時停止および再開する協調マルチスレッドのみを実装できます。コルーチンは、必要に応じて中断と再開の懸念のみを特定しますが、コルーチンディスパッチャーは、適切なスレッドで開始および再開することを保証します。

このコードを調べると、Kotlinコルーチンの本質を理解するのに役立ちます。

_import kotlinx.coroutines.experimental.*
import kotlin.coroutines.experimental.*

fun main(args: Array<String>) {
    var continuation: Continuation<Unit>? = null
    println("main(): launch")
    GlobalScope.launch(Dispatchers.Unconfined) {
        println("Coroutine: started")
        suspendCoroutine<Unit> {
            println("Coroutine: suspended")
            continuation = it
        }
        println("Coroutine: resumed")
    }
    println("main(): resume continuation")
    continuation!!.resume(Unit)
    println("main(): back after resume")
}
_

ここでは、最も単純なUnconfinedディスパッチャを使用しています。これは、ディスパッチを行わず、_launch { ... }_とcontinuation.resume()を呼び出すコルーチンを実行します。コルーチンはsuspendCoroutineを呼び出すことによって自身を一時停止します。この関数は、後でコルーチンを再開するために使用できるオブジェクトを渡して、指定したブロックを実行します。私たちのコードはそれを_var continuation_に保存します。制御はlaunchの後のコードに戻ります。ここでは、継続オブジェクトを使用してコルーチンを再開します。

プログラム全体がメインスレッドで実行され、次のように出力されます。

_main(): launch
Coroutine: started
Coroutine: suspended
main(): resume continuation
Coroutine: resumed
main(): back after resume
_

1つのコルーチンを起動してREST API呼び出しを行ってデータを取得する一方で、別のコルーチンを使用して、データに依存しない残りのUIをペイントすることができます。

これは実際にプレーンスレッドで何をするかを説明しています。コルーチンの利点は、GUIにバインドされたコードの途中で「ブロッキング」呼び出しを行うことができ、GUIがフリーズしないことです。あなたの例では、ネットワーク呼び出しを行い、GUIを更新する単一のコルーチンを記述します。ネットワーク要求の進行中は、コルーチンが一時停止され、他のイベントハンドラーが実行され、GUIが生きたままになります。ハンドラーはコルーチンではなく、単なる通常のGUIコールバックです。

簡単に言えば、Androidコード:

_activity.launch(Dispatchers.Main) {
    textView.text = requestStringFromNetwork()
}

...

suspend fun requestStringFromNetwork() = suspendCancellableCoroutine<String> {
    ...
}
_

requestStringFromNetworkは「IOレイヤーをパッチする」と同等ですが、実際にはパッチを適用せず、IO =ライブラリのパブリックAPI。ほとんどすべてのKotlin IOライブラリがこれらのラッパーを追加しており、Java IO libs。 これらの手順 に従っている場合、独自のライブラリを作成することも非常に簡単です。

6
Marko Topolnik

答えは、コルーチンはネットワークコールやI/O操作を認識していません。デフォルトの動作は連続しているため、重い作業を異なるコルーチンに囲んで、必要に応じてコードを記述し、それらを同時に実行できるようにする必要があります。

例えば:

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) // pretend we are doing something useful here (maybe I/O)
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(1000L) // pretend we are doing something useful here (maybe I/O), too
    return 29
}

fun main(args: Array<String>) = runBlocking<Unit> {
        val time = measureTimeMillis {
            val one = doSomethingUsefulOne()
            val two = doSomethingUsefulTwo()
            println("The answer is ${one + two}")
        }
    println("Completed in $time ms")
}

このようなものを生成します:

The answer is 42
Completed in 2017 ms

そしてdoSomethingUsefulOne()とdoSomethingUsefulTwo()は順次実行されます。同時実行が必要な場合は、代わりに次のように記述する必要があります。

fun main(args: Array<String>) = runBlocking<Unit> {
    val time = measureTimeMillis {
        val one = async { doSomethingUsefulOne() }
        val two = async { doSomethingUsefulTwo() }
        println("The answer is ${one.await() + two.await()}")
    }
    println("Completed in $time ms")
}

それは生成されます:

The answer is 42
Completed in 1017 ms

doSomethingUsefulOne()およびdoSomethingUsefulTwo()は同時に実行されるため。

ソース: https://github.com/Kotlin/kotlinx.coroutines/blob/master/coroutines-guide.md#composing-suspending-functions

UPDATE:コルーチンが実行される場所については、githubプロジェクトガイド https://github.com/Kotlin/kotlinx。 coroutines/blob/master/coroutines-guide.md#thread-local-data

スレッドローカルデータを渡す機能があると便利な場合がありますが、特定のスレッドにバインドされていないコルーチンの場合、大量のボイラープレートを記述せずに手動で実現するのは困難です。

ThreadLocalの場合は、asContextElement拡張関数が役立ちます。追加のコンテキスト要素を作成し、指定されたThreadLocalの値を保持し、コルーチンがコンテキストを切り替えるたびにそれを復元します。

実際にそれを示すのは簡単です:

val threadLocal = ThreadLocal<String?>() // declare thread-local variable
fun main(args: Array<String>) = runBlocking<Unit> {
    threadLocal.set("main")
    println("Pre-main, current thread: ${Thread.currentThread()}, threadlocal 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")、コルーチンが実行されるスレッドに関係なく。したがって、出力(デバッグあり)は次のとおりです。

Pre-main, current thread: Thread[main @coroutine#1,5,main], thread local value: 'main'
Launch start, current thread: Thread[CommonPool-worker-1 @coroutine#2,5,main], thread local value: 'launch'
After yield, current thread: Thread[CommonPool-worker-2 @coroutine#2,5,main], thread local value: 'launch'
Post-main, current thread: Thread[main @coroutine#1,5,main], thread local value: 'main'
2
Raymond Arteaga