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)
}
ThreadPool
prédéfinie au début. (A)
, Kotlin commence à exécuter la coroutine dans le prochain fil disponible (Say Thread01
).(B)
, Kotlin arrête d'exécuter le thread en cours et lance la fonction suspendue loadData()
dans le prochain thread disponible disponible (Thread02
).(B)
revient après exécution, Kotlin continue la coroutine dans le prochain fil libre disponible (Thread03
).(C)
s'exécute sur Thread03
.(D)
, le Thread03
est arrêté.(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?
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 fun
s. 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)
}
- 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.
- À
(A)
, Kotlin commence à exécuter la coroutine dans le prochain fil libre disponible (SayThread01
).
Il peut également s'agir du même fil où vous avez appelé launch
.
- Au
(B)
, Kotlin cesse d'exécuter le thread actuel et lance la fonction suspendueloadData()
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.
- 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.
(C)
s'exécute surThread03
.- À
(D)
, leThread03
est arrêté.- Après 1000 ms,
(E)
est exécuté sur le prochain thread libre, par exempleThread01
.
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) }
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.