web-dev-qa-db-fra.com

Comment les coroutines Kotlin savent-elles quand céder lors des appels réseau?

Je suis nouveau dans les routines Kotlin et je n’ai pas compris ce qui m’a été fait: comment les coroutines savent-elles quand céder le pas aux autres lors des appels réseau?.

Si je comprends bien, une coroutine fonctionne de manière préventive, ce qui signifie qu’elle sait quand céder le pas à d’autres routines quand elle a des tâches fastidieuses (généralement des opérations d’E/S).

Par exemple, disons que nous voulons peindre une interface utilisateur qui affichera les données d'un serveur distant, et que nous ne disposons que d'un seul thread pour planifier nos coroutines. Nous pourrions lancer une coroutine pour effectuer des appels d'API REST afin d'obtenir les données, tout en disposant d'une autre coroutine Peindre le reste de l'interface utilisateur qui ne dépend pas des données. Cependant, comme nous n’avons qu’un seul thread, il ne peut y avoir qu’une seule coroutine à la fois. Et à moins que la coroutine utilisée pour extraire les données par anticipation cède pendant qu'elle attend l'arrivée des données, les deux coroutines seront exécutées de manière séquentielle.

Autant que je sache, l'implémentation de la coroutine de Kotlin ne corrige aucune des implémentations JVM ni des bibliothèques de réseau JDK existantes. Ainsi, si une coroutine appelle une API REST, elle doit être bloquée comme si cela se faisait à l'aide d'un thread Java. Je dis cela parce que j'ai l'impression que des concepts similaires en python s'appellent des fils verts. Et pour que cela fonctionne avec la bibliothèque réseau intégrée de python, il faut d'abord «patcher» la bibliothèque réseau. Et pour moi, cela a du sens car seule la bibliothèque réseau elle-même sait quand céder.

Alors, est-ce que quelqu'un pourrait expliquer comment la coroutine de Kotlin sait quand céder le pas lorsque vous appelez des API de réseau Java bloquantes? Ou si ce n'est pas le cas, cela signifie-t-il que les tâches mentionnées dans l'exemple ci-dessus ne peuvent pas être exécutées simultanément pour donner un seul thread?

Merci!

7
Rafoul

une coroutine fonctionne à titre préventif

Nan. Avec les coroutines, vous ne pouvez implémenter que le multithreading coopératif, où vous suspendez et reprenez des routines avec des appels de méthodes explicites. La coroutine souligne simplement le souci de suspendre et de reprendre à la demande, tandis que le répartiteur coroutine est chargé de s’assurer qu’il commence et reprend sur le fil approprié.

L'étude de ce code vous aidera à comprendre l'essence des coroutines 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")
}

Nous utilisons ici le dispatcher le plus trivial Unconfined, qui ne fait pas de dispatch, il exécute la coroutine là où vous appelez launch { ... } et continuation.resume(). La coroutine se suspend en appelant suspendCoroutine. Cette fonction exécute le bloc que vous avez fourni en lui transmettant l’objet que vous pourrez utiliser ultérieurement pour reprendre la coroutine. Notre code l'enregistre dans le var continuation. Control retourne au code après launch, où nous utilisons l’objet continuation pour reprendre la coroutine.

L'ensemble du programme s'exécute sur le thread principal et affiche ceci:

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

Nous pourrions lancer une coroutine pour effectuer des appels d'API REST afin d'obtenir les données, tout en disposant d'une autre coroutine Peindre le reste de l'interface utilisateur qui ne dépend pas des données.

Cela décrit en fait ce que vous feriez avec des threads simples. L'avantage des coroutines est que vous pouvez effectuer un appel "bloquant" au milieu d'un code lié à une interface graphique, sans geler l'interface graphique. Dans votre exemple, vous écrivez une seule coroutine qui passe l'appel réseau, puis met à jour l'interface graphique. Pendant que la demande de réseau est en cours, la coroutine est suspendue et d'autres gestionnaires d'événements sont exécutés, ce qui maintient l'interface graphique active. Les gestionnaires ne sont pas des routines, ils ne sont que des rappels réguliers par une interface graphique.

En termes simples, vous pouvez écrire ce code Android:

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

...

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

requestStringFromNetwork est l'équivalent de "corriger la couche IO", mais vous ne corrigez rien, vous écrivez simplement des enveloppes autour de l'API publique de la bibliothèque IO. Presque toutes les bibliothèques Kotlin IO ajoutent ces wrappers et il existe également des bibliothèques d'extension pour les bibliothèques Java IO. Il est également très simple d’écrire le vôtre si vous suivez ces instructions .

4
Marko Topolnik

La réponse est: Coroutine ne sait rien des appels réseau ou des opérations d’E/S. Vous devez écrire le code selon votre choix, en englobant des tâches lourdes dans différentes coroutines afin qu'elles puissent être exécutées simultanément car le comportement par défaut est séquentiel.

Par exemple:

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")
}

produira quelque chose comme ceci:

The answer is 42
Completed in 2017 ms

et doSomethingUsefulOne () et doSomethingUsefulTwo () seront exécutés séquentiellement . Si vous voulez une exécution simultanée, vous devez écrire à la place:

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")
}

cela produira:

The answer is 42
Completed in 1017 ms

comme doSomethingUsefulOne () et doSomethingUsefulTwo () seront exécutés simultanément.

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

UPDATE: Pour savoir où sont exécutées les coroutines, consultez le guide de projet github https://github.com/Kotlin/kotlinx.coroutines/blob/master/coroutines-guide.md#thread -local-data :

Il est parfois pratique de pouvoir transmettre des données de threads locales, mais, pour les coroutines, qui ne sont liées à aucun thread particulier, il est difficile de les réaliser manuellement sans écrire beaucoup de passe-passe.

Pour ThreadLocal, la fonction d'extension asContextElement est ici pour le sauvetage. Il crée un élément de contexte supplémentaire, qui conserve la valeur du ThreadLocal donné et le restaure chaque fois que la coroutine change de contexte.

Il est facile de le démontrer en action:

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()}'")
}

Dans cet exemple, nous lançons une nouvelle coroutine dans un pool de threads d’arrière-plan à l’aide de Dispatchers.Default. Elle fonctionne donc sur des threads différents d’un pool de threads, tout en conservant la valeur de variable locale de thread que nous avons spécifiée à l’aide de value = "launch"), peu importe le fil d'exécution de la coroutine. Ainsi, la sortie (avec le débogage) est:

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'
1
Raymond Arteaga