web-dev-qa-db-fra.com

Comment les coroutines Kotlin fonctionnent-elles en interne?

Comment Kotlin met-il en œuvre des coroutines en interne?

Les Coroutines sont considérées comme une "version plus légère" des threads, et je comprends qu'elles utilisent des threads en interne pour exécuter des coroutines.

Que se passe-t-il lorsque je démarre une coroutine en utilisant l'une des fonctions du générateur? 

Ceci est ma compréhension de l'exécution de ce code: 

GlobalScope.launch {       <---- (A)
    val y = loadData()     <---- (B)  // suspend fun loadData() 
    println(y)             <---- (C)
    delay(1000)            <---- (D)
    println("completed")   <---- (E)
}
  1. Kotlin a une ThreadPool prédéfinie au début. 
  2. À (A), Kotlin commence à exécuter la coroutine dans le prochain fil disponible (Say Thread01).
  3. En (B), Kotlin arrête d'exécuter le thread en cours et lance la fonction suspendue loadData() dans le prochain thread disponible disponible (Thread02).
  4. Lorsque (B) revient après exécution, Kotlin continue la coroutine dans le prochain fil libre disponible (Thread03).
  5. (C) s'exécute sur Thread03.
  6. À (D), le Thread03 est arrêté.
  7. Après 1000 ms, (E) est exécuté sur le prochain thread libre, par exemple Thread01.

Est-ce que je comprends bien? Ou les coroutines sont-elles mises en œuvre de manière différente?

7
Vishnu Haridas

Les coroutines sont une chose complètement distincte de toute politique de planification que vous décrivez. Une coroutine est essentiellement une chaîne d’appel de suspend funs. La suspension est totalement sous votre contrôle: il vous suffit d'appeler suspendCoroutine. Vous obtiendrez un objet de rappel afin que vous puissiez appeler sa méthode resume et revenir à l'endroit où vous avez suspendu.

Voici un code où vous pouvez voir que la suspension est un mécanisme très direct et transparent, entièrement sous votre contrôle:

import kotlin.coroutines.*
import kotlinx.coroutines.*

var continuation: Continuation<String>? = null

fun main(args: Array<String>) {
    val job = GlobalScope.launch(Dispatchers.Unconfined) {
        while (true) {
            println(suspendHere())
        }
    }
    continuation!!.resume("Resumed first time")
    continuation!!.resume("Resumed second time")
}

suspend fun suspendHere() = suspendCancellableCoroutine<String> {
    continuation = it
}

La coroutine vous launch se suspend chaque fois qu’elle appelle suspendHere(). Il écrit le rappel de continuation dans la propriété continuation, puis vous utilisez explicitement cette continuation pour reprendre la coroutine.

Le code utilise le dispatcher Unconfined coroutine qui ne distribue pas de threads du tout, il exécute simplement le code de coroutine là où vous appelez continuation.resume().


Dans cet esprit, revoyons votre diagramme:

GlobalScope.launch {       <---- (A)
    val y = loadData()     <---- (B)  // suspend fun loadData() 
    println(y)             <---- (C)
    delay(1000)            <---- (D)
    println("completed")   <---- (E)
}
  1. Kotlin a une ThreadPool prédéfinie au début. 

Il peut ou peut ne pas avoir un pool de threads. Un répartiteur d'interface utilisateur fonctionne avec un seul thread. 

Pour qu'un thread soit la cible d'un répartiteur de coroutine, une file d'attente simultanée lui est associée. Il exécute une boucle de niveau supérieur qui extrait les objets Runnable de cette file d'attente et les exécute. Un répartiteur de coroutine met simplement la suite dans cette file.

  1. À (A), Kotlin commence à exécuter la coroutine dans le prochain fil libre disponible (Say Thread01).

Il peut également s'agir du même fil où vous avez appelé launch.

  1. Au (B), Kotlin cesse d'exécuter le thread actuel et lance la fonction suspendue loadData() dans le prochain thread disponible disponible (Thread02).

Kotlin n'a pas besoin d'arrêter les threads pour suspendre une coroutine. En fait, l’intérêt principal des coroutines est que les threads ne pas soient démarrés ou arrêtés. La boucle de niveau supérieur du thread continuera et choisira un autre exécutable à exécuter.

De plus, le simple fait que vous appelez un suspend fun n'a aucune signification. La coroutine ne se suspendra que lorsqu'elle aura explicitement appelé suspendCoroutine. La fonction peut aussi simplement revenir sans suspension.

Mais supposons que cela a appelé suspendCoroutine. Dans ce cas, la coroutine ne lance plus sur aucun thread. Il est suspendu et ne peut pas continuer tant que du code, quelque part, n’appelle pas continuation.resume(). Ce code pourrait être exécuté sur n'importe quel thread, à tout moment dans le futur.

  1. Lorsque (B) revient après exécution, Kotlin continue la coroutine dans le prochain fil libre disponible (Thread03).

B ne "revient pas après l'exécution", la coroutine reprend alors qu'elle est toujours dans son corps. Il peut suspendre et reprendre un nombre quelconque de fois avant de revenir.

  1. (C) s'exécute sur Thread03.
  2. À (D), le Thread03 est arrêté.
  3. Après 1000 ms, (E) est exécuté sur le prochain thread libre, par exemple Thread01.

Encore une fois, aucun thread n'est arrêté. La coroutine est suspendue et un mécanisme, généralement spécifique au répartiteur, est utilisé pour planifier sa reprise après 1000 ms. À ce stade, il sera ajouté à la file d'attente d'exécution associée au répartiteur.


Pour plus de détails, voyons quelques exemples du type de code nécessaire pour envoyer une coroutine.

Répartiteur d'interface utilisateur:

EventQueue.invokeLater { continuation.resume(value) }

Répartiteur d'interface utilisateur Android:

mainHandler.post { continuation.resume(value) }

Répartiteur ExecutorService:

executor.submit { continuation.resume(value) } 
5
Marko Topolnik

Les coroutines fonctionnent en créant un commutateur sur les points de reprise possibles:

class MyClass$Coroutine extends CoroutineImpl {
    public Object doResume(Object o, Throwable t) {
        switch(super.state) {
        default:
                throw new IllegalStateException("call to \"resume\" before \"invoke\" with coroutine");
        case 0:  {
             // code before first suspension
             state = 1; // or something else depending on your branching
             break;
        }
        case 1: {
            ...
        }
        }
        return null;
    }
}

Le code résultant de l'exécution de cette coroutine crée alors cette instance et appelle la fonction doResume() chaque fois qu'elle doit reprendre son exécution. La façon dont cela est traité dépend du planificateur utilisé pour l'exécution.

Voici un exemple de compilation pour une coroutine simple:

launch {
    println("Before")
    delay(1000)
    println("After")
}

Qui compile ce bytecode

private kotlinx.coroutines.experimental.CoroutineScope p$;

public final Java.lang.Object doResume(Java.lang.Object, Java.lang.Throwable);
Code:
   0: invokestatic  #18                 // Method kotlin/coroutines/experimental/intrinsics/IntrinsicsKt.getCOROUTINE_SUSPENDED:()Ljava/lang/Object;
   3: astore        5
   5: aload_0
   6: getfield      #22                 // Field kotlin/coroutines/experimental/jvm/internal/CoroutineImpl.label:I
   9: tableswitch   { // 0 to 1
                 0: 32
                 1: 77
           default: 102
      }
  32: aload_2
  33: dup
  34: ifnull        38
  37: athrow
  38: pop
  39: aload_0
  40: getfield      #24                 // Field p$:Lkotlinx/coroutines/experimental/CoroutineScope;
  43: astore_3
  44: ldc           #26                 // String Before
  46: astore        4
  48: getstatic     #32                 // Field Java/lang/System.out:Ljava/io/PrintStream;
  51: aload         4
  53: invokevirtual #38                 // Method Java/io/PrintStream.println:(Ljava/lang/Object;)V
  56: sipush        1000
  59: aload_0
  60: aload_0
  61: iconst_1
  62: putfield      #22                 // Field kotlin/coroutines/experimental/jvm/internal/CoroutineImpl.label:I
  65: invokestatic  #44                 // Method kotlinx/coroutines/experimental/DelayKt.delay:(ILkotlin/coroutines/experimental/Continuation;)Ljava/lang/Object;
  68: dup
  69: aload         5
  71: if_acmpne     85
  74: aload         5
  76: areturn
  77: aload_2
  78: dup
  79: ifnull        83
  82: athrow
  83: pop
  84: aload_1
  85: pop
  86: ldc           #46                 // String After
  88: astore        4
  90: getstatic     #32                 // Field Java/lang/System.out:Ljava/io/PrintStream;
  93: aload         4
  95: invokevirtual #38                 // Method Java/io/PrintStream.println:(Ljava/lang/Object;)V
  98: getstatic     #52                 // Field kotlin/Unit.INSTANCE:Lkotlin/Unit;
 101: areturn
 102: new           #54                 // class Java/lang/IllegalStateException
 105: dup
 106: ldc           #56                 // String call to \'resume\' before \'invoke\' with coroutine
 108: invokespecial #60                 // Method Java/lang/IllegalStateException."<init>":(Ljava/lang/String;)V
 111: athrow

J'ai compilé cela avec kotlinc 1.2.41

De 32 à 76 est le code pour imprimer Before et appeler delay(1000) qui suspend.

De 77 à 101 correspond le code pour imprimer After.

Le traitement des erreurs pour les états de reprise illégaux est compris entre 102 et 111, comme indiqué par l’étiquette default dans la table des commutateurs.

En résumé, les coroutines de kotlin sont simplement des machines à états contrôlées par un planificateur.

1
Minn