Coroutine Lua
Qu'est-ce qu'une coroutine ?
Les coroutines Lua sont assez similaires aux threads : elles ont des piles indépendantes, des variables locales indépendantes et des pointeurs d'instruction indépendants, tout en partageant également des variables globales et la plupart des autres choses avec d'autres coroutines.
Une coroutine peut être comprise comme un type spécial de thread qui peut suspendre et reprendre son exécution, permettant ainsi un multitâche non préemptif.
Les coroutines sont une fonctionnalité très puissante, mais elles sont aussi complexes à utiliser.
Syntaxe de base
Les coroutines sont prises en charge par le modulecoroutine.
Lorsque vous utilisez des coroutines, vous pouvez utilisercoroutine.createpour créer un nouvel objet coroutine et utilisercoroutine.resumepour démarrer son exécution. Une coroutine peutcoroutine.yieldsuspendre volontairement sa propre exécution et rendre le contrôle à l’appelant.
| Méthode | Description |
|---|---|
| coroutine.create() | Crée une coroutine, renvoie une coroutine, le paramètre est une fonction. Lorsqu’elle est utilisée avec resume, elle réveille l’appel de fonction. |
| coroutine.resume() | Redémarre une coroutine, utilisée avec create. |
| coroutine.yield() | Suspend une coroutine, place la coroutine dans l’état suspendu. Utilisée avec resume, elle peut avoir de nombreux effets utiles. |
| coroutine.status() | Vérifie l’état d’une coroutine. Remarque : une coroutine a trois états : dead, suspended, running. Voir le programme ci-dessous pour savoir quand ces états se produisent. |
| coroutine.wrap() | Crée une coroutine, renvoie une fonction. Une fois cette fonction appelée, vous entrez dans la coroutine. Cela duplique la fonctionnalité de create. |
| coroutine.running() | Renvoie la coroutine en cours d'exécution. Une coroutine est un thread. Lorsque vous utilisez running, il renvoie l'ID de thread d'une coroutine. |
L'exemple suivant montre comment utiliser les coroutines Lua :
Exemple
print("La coroutine foo a commencé son exécution")
local value = coroutine.yield("Mise en pause de l'exécution de foo")
print("La coroutine foo a repris son exécution, la valeur transmise est : " .. tostring(value))
print("La coroutine foo a terminé son exécution")
end
-- Créer une coroutine
local co = coroutine.create(foo)
-- Démarrer la coroutine
local status, result = coroutine.resume(co)
print(result) -- Sortie : mise en pause de l'exécution de foo
-- Reprendre l'exécution de la coroutine et passer une valeur
status, result = coroutine.resume(co, 42)
print(result) -- Sortie : la coroutine foo a repris son exécution, la valeur transmise est : 42
Dans l'exemple ci-dessus, nous avons défini une fonction nommée foo comme coroutine. À l'intérieur de la fonction, nous avons utilisé coroutine.yield pour suspendre l'exécution de la coroutine et renvoyer une valeur.
Dans le programme principal, nous avons utilisé coroutine.create pour créer un objet coroutine, et coroutine.resume pour démarrer son exécution.
Après le premier appel à coroutine.resume, la coroutine se met en pause à coroutine.yield et renvoie la valeur au programme principal. Ensuite, nous appelons coroutine.resume à nouveau, en passant une valeur comme paramètre pour que la coroutine reprenne son exécution.
L'exécution du code ci-dessus produit la sortie suivante :
协同程序 foo 开始执行 暂停 foo 的执行 协同程序 foo 恢复执行,传入的值为: 42 协同程序 foo 结束执行 nil
Il convient de noter que l'état d'une coroutine peut être obtenu via la fonction coroutine.status. En vérifiant l'état, vous pouvez déterminer l'état d'exécution de la coroutine (comme en cours d'exécution, suspendue, terminée, etc.).
L'exemple suivant montre l'utilisation des méthodes ci-dessus :
fichier coroutine_test.lua
-- Crée un nouvel objet coroutine co, où la fonction de coroutine affiche le paramètre transmis i
co = coroutine.create(
fonction(i)
print(i);
end
)
-- Utilisez coroutine.resume pour démarrer l'exécution de la coroutine co, en passant le paramètre 1. La coroutine commence à s'exécuter et affiche la sortie 1.
coroutine.resume(co, 1) -- 1
-- Vérifiez l'état de la coroutine co via coroutine.status, la sortie est dead, indiquant que la coroutine a terminé son exécution.
print(coroutine.status(co)) -- dead
print("----------")
-- Utilisez coroutine.wrap pour créer un wrapper de coroutine, en convertissant la fonction de coroutine en un objet fonction directement appelable.
co = coroutine.wrap(
fonction(i)
print(i);
end
)
co(1)
print("----------")
-- Créez un autre objet coroutine co2, où la fonction de coroutine affiche les nombres de 1 à 10 dans une boucle, et lorsque la boucle atteint 3, elle affiche l'état actuel de la coroutine et le thread en cours d'exécution.
co2 = coroutine.create(
fonction()
for i=1,10 do
print(i)
if i == 3 then
print(coroutine.status(co2)) --running
print(coroutine.running()) --thread:XXXXXX
end
coroutine.yield()
end
end
)
-- Appelez coroutine.resume plusieurs fois pour démarrer l'exécution de la coroutine co2.
coroutine.resume(co2) --1
coroutine.resume(co2) --2
coroutine.resume(co2) --3
-- Vérifiez l'état de la coroutine co2 via coroutine.status, la sortie est suspended, ce qui indique que la coroutine est en pause.
print(coroutine.status(co2)) -- suspended
print(coroutine.running())
print("----------")
L'exemple ci-dessus produit la sortie suivante :
1 dead ---------- 1 ---------- 1 2 3 running thread: 0x7fb801c05868 false suspended thread: 0x7fb801c04c88 true ----------
Comme on peut le voir avec coroutine.running, une coroutine est implémentée comme un thread au niveau sous-jacent.
Lorsque vous créez une coroutine, elle enregistre un événement dans un nouveau thread.
Lorsque resume est utilisé pour déclencher l'événement, la fonction coroutine créée par create est exécutée. Lorsque yield est rencontré, cela signifie suspendre le thread actuel et attendre un autre resume pour déclencher l'événement.
Ensuite, analysons un exemple plus détaillé :
Exemple
print("sortie de la fonction foo", a)
return coroutine.yield(2 * a) -- Renvoie la valeur de 2*a
end
co = coroutine.create(function (a , b)
print("Sortie de la première exécution de la coroutine", a, b) -- co-body 1 10
local r = foo(a + 1)
print("Sortie de la deuxième exécution de la coroutine", r)
local r, s = coroutine.yield(a + b, a - b) -- Les valeurs de a et b sont passées lorsque la coroutine est appelée pour la première fois
print("Sortie de la troisième exécution de la coroutine", r, s)
return b, "Fin de la coroutine" -- La valeur de b est passée lorsque la coroutine est appelée pour la deuxième fois
end)
print("principal", coroutine.resume(co, 1, 10)) -- true, 4
print("--Séparateur----")
print("principal", coroutine.resume(co, "r")) -- true 11 -9
print("---Separator---")
print("main", coroutine.resume(co, "x", "y")) -- true 10 end
print("---Separator---")
print("main", coroutine.resume(co, "x", "y")) -- impossible de relancer une coroutine morte
print("---Separator---")
L'exemple ci-dessus produit la sortie suivante :
第一次协同程序执行输出 1 10 foo 函数输出 2 main true 4 --分割线---- 第二次协同程序执行输出 r main true 11 -9 ---分割线--- 第三次协同程序执行输出 x y main true 10 结束协同程序 ---分割线--- main false cannot resume dead coroutine ---分割线---
L'exemple ci-dessus se déroule comme suit :
- Appelez resume pour réveiller la coroutine. L'opération resume renvoie true en cas de succès, sinon elle renvoie false.
- La coroutine s'exécute.
- S'exécute jusqu'à l'instruction yield.
- yield suspend la coroutine, et le premier resume renvoie. (Remarque : ici yield renvoie, et les paramètres sont les arguments de resume).
- Le deuxième resume réveille à nouveau la coroutine. (Remarque : parmi les arguments de resume ici, à l'exception du premier argument, les arguments restants serviront de paramètres à yield).
- yield renvoie.
- La coroutine continue de s'exécuter.
- Si, après que la coroutine a fini de s'exécuter, vous continuez à appeler la méthode resume, elle affichera : cannot resume dead coroutine.
La puissance de la combinaison de resume et yield réside dans le fait que resume est dans le programme principal et transmet l'état externe (les données) dans la coroutine, tandis que yield renvoie l'état interne (les données) au programme principal.
Problème producteur-consommateur
Maintenant, je vais utiliser les coroutines Lua pour résoudre le problème classique producteur-consommateur.
Exemple
function productor()
local i = 0
while true do
i = i + 1
send(i) -- Envoyer l'élément produit au consommateur
end
end
function consumer()
while true do
local i = receive() -- Obtenir un élément du producteur
print(i)
end
end
function receive()
local status, value = coroutine.resume(newProductor)
return value
end
function send(x)
coroutine.yield(x) -- x représente la valeur à envoyer. Une fois la valeur renvoyée, suspendre la coroutine.
end
-- Démarrer le programme
newProductor = coroutine.create(productor)
consumer()
L'exemple ci-dessus produit la sortie suivante :
1 2 3 4 5 6 7 8 9 10 11 12 13 ……
Différence entre les threads et les coroutines
La principale différence entre les threads et les coroutines est qu'un programme comportant plusieurs threads peut exécuter plusieurs threads en même temps, tandis que les coroutines doivent s'exécuter en coopération les unes avec les autres.
Une seule coroutine s'exécute à un instant donné, et la coroutine en cours d'exécution n'est suspendue que lorsqu'on lui demande explicitement de se suspendre.
Les coroutines sont un peu comme du multithreading synchrone. Plusieurs threads en attente du même verrou de thread sont quelque peu similaires aux coroutines.
Les principales différences sont résumées comme suit :
Méthode d'ordonnancement : les threads sont généralement ordonnancés de manière préemptive par l'ordonnanceur du système d'exploitation, qui commute les droits d'exécution entre les différents threads. Les coroutines, en revanche, sont ordonnancées de manière non préemptive ; les programmeurs contrôlent explicitement le transfert des droits d'exécution.
Concurrence : les threads s'exécutent de manière concurrente ; plusieurs threads peuvent s'exécuter simultanément sur plusieurs cœurs de processeur, ou basculer l'exécution sur un seul cœur via le découpage temporel. Les coroutines sont coopératives : une seule coroutine s'exécute à la fois, et les autres coroutines doivent attendre que la coroutine en cours d'exécution cède volontairement l'exécution.
Utilisation de la mémoire : les threads nécessitent généralement des piles et des contextes d'exécution séparés, de sorte que la création et la destruction de threads entraînent un surcoût supplémentaire. Les coroutines peuvent partager la même pile et le même contexte, de sorte que le surcoût de création et de destruction des coroutines est plus faible.
Partage de données : les threads peuvent partager l'espace mémoire, mais les problèmes de sécurité des threads et de synchronisation doivent être pris en compte. Les coroutines partagent généralement les données par le passage de paramètres et les valeurs de retour, et l'isolation des données entre différentes coroutines est meilleure.
Débogage et gestion des erreurs : les threads sont généralement plus complexes à déboguer et à gérer les erreurs, car les interactions et l'exécution concurrente entre plusieurs threads peuvent entraîner des problèmes difficiles à déboguer. Les coroutines sont relativement plus simples à déboguer et à gérer les erreurs, car les programmeurs contrôlent explicitement le flux d'exécution.
Dans l'ensemble, les threads conviennent aux scénarios nécessitant une exécution concurrente, comme l'utilisation du parallélisme sur des processeurs multicœurs pour accélérer l'exécution des tâches. Les coroutines conviennent aux scénarios nécessitant collaboration et coordination, comme les machines à états, la programmation événementielle ou le traitement coopératif des tâches. Choisir entre threads et coroutines dépend des exigences spécifiques de l'application et du modèle de programmation.
Autres extensions