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éthodeDescription
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

function foo()
    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

-- 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

function foo (a)
    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

local newProductor

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