Un des exemples d'utilisation des coroutines, est celui de production --> consommation!...
Explication.
Supposez une fonction qui produit continuellement les valeurs (par exemple, la lecture d'un fichier) et une autre fonction qui consomme en permanence ces valeurs (par exemple, en écrivant les valeurs récupérées dans un autre fichier).
Typiquement, ces deux fonctions pourraient ressembler à celles-ci:
function producteur() while true do local x = io.read() -- production d'une nouvelle valeur. envoyer(x) -- envoie à consommateur. end end function consommateur() while true do local x = recevoir() -- reçoit de producteur. io.write(x, "\n") -- utilise la nouvelle valeur. end end
Dans cette façon de faire, le producteur et le consommateur tournent continuellement, mais il reste facile de les arrêter lorsqu'il n'y aura plus de données.
Le problème ici est: " comment faire correspondre envoyer et recevoir? "
C'est un cas typique du problème de " qui a la main sur la boucle... "
Tant que le producteur et le consommateur sont actifs, les deux ont leurs propres boucles principales, et les deux supposent que l'autre est un service appelable.
Dans cet exemple particulier, il serait facile de modifier la structure de l'une des fonctions, en déroulant sa boucle pour en faire un agent passif.
Toutefois, ce changement de structure pourrait être plus difficile dans d'autres scénarios réels.
Les coroutines fournissent un outil idéal pour faire correspondre producteur et consommateur, car une paire de resume-yield tourne à l'envers la relation typique entre l'appelant et l'appelé.
Quand une coroutine appelle yield , elle n'entre pas dans une nouvelle fonction, mais retourne plutôt un appel en attente (pour resume).
De même, un appel à resume ne démarre pas une nouvelle fonction, mais retourne un appel à yield.
Cette propriété est exactement celle dont vous avez besoin pour faire correspondre envoyer à recevoir, de telle sorte que chacun agisse comme s'il était le maître et l'autre l'esclave...
Donc, recevoir des resume de producteur afin qu'il puisse produire une nouvelle valeur, et envoyer des yield de la nouvelle valeur pour le consommateur.
function recevoir() local status, value = coroutine.resume(producteur) return value end function envoyer(x) coroutine.yield(x) end
producteur = coroutine.create( function() while true do local x = io.read() -- produit une nouvelle valeur envoyer(x) end end)
Dans cette conception, le programme appelle en premier la fonction recevoir()
Lorsque celle-ci a besoin d'un élément, elle fait appel à coroutine.resume(), qui boucle jusqu'à ce qu'elle ait quelque chose à envoyer(x), puis s'arrête jusqu'au redémarrage de recevoir()
Il s'agit là d'une conception axée sur la réception...
Vous pouvez étendre cette conception de "filtres", qui ne sont en fait que des tâches qui se placent entre le producteur et le consommateur afin d'opérer une sorte de transformation dans les données.
Un filtre va chercher (resume) sur un producteur pour obtenir de nouvelles valeurs et retourne (yield) des valeurs transformées à un consommateur.
(Un filtre resume sur un producteur pour obtenir de nouvelles valeurs et yield des valeurs transformées à un consommateur.)
Vous pouvez aussi, ajouter au code précédent, un filtre qui insère un numéro de ligne au début de chaque ligne.
Le code complet, pourrait alors ressembler à ceci:
function receive(prod) local status, value = coroutine.resume(prod) return value end function send(x) coroutine.yield(x) end function producer() return coroutine.create(function() while true do local x = io.read() -- produit une nouvelle valeur send(x) end end) end function filter(prod) return coroutine.create(function() local line = 1 while true do local x = receive(prod) -- obtient une nouvelle valeur x = string.format("%5d %s", line, x) send(x) -- envoie au consommateur line = line + 1 end end) end function consumer (prod) while true do local x = receive(prod) -- obtient une nouvelle valeur io.write(x, "\n") -- consomme une nouvelle valeur end end
Le dernier bit crée simplement les composants dont il a besoin, les relie, et commence le consommateur final:
Les coroutines sont une sorte de multithreading non-préemptif*.
Alors que dans les pipes, chaque tâche s'exécute dans un processus séparé, les coroutines elles, exécutent chaque tâche dans une coroutine séparée.
Les pipes fournissent une zone tampon entre l'écrivain (producteur) et le lecteur (la consommation) ce qui laisse une certaine liberté dans leurs vitesses relatives.
Ceci est important dans le contexte de pipes, car le coût de la commutation entre les processus est élevé.
Avec les coroutines, le coût de la commutation entre les différentes tâches est beaucoup plus petit... environ le même que lors d'un appel à une fonction.