Pourquoi les coroutines sont de retour? [fermé]

19

La plupart des travaux préparatoires pour les coroutines ont eu lieu dans les années 60/70, puis se sont arrêtés en faveur d'alternatives (par exemple, les fils)

Y a-t-il une substance au regain d'intérêt pour les coroutines qui s'est produit en python et dans d'autres langages?

user1787812
la source
9
Je ne suis pas sûr qu'ils soient jamais partis.
Blrfl

Réponses:

26

Les Coroutines ne sont jamais parties, elles ont juste été éclipsées par d'autres choses en attendant. L'intérêt récemment accru pour la programmation asynchrone et donc les coroutines est en grande partie dû à trois facteurs: une acceptation accrue des techniques de programmation fonctionnelle, des ensembles d'outils avec un faible support pour le vrai parallélisme (JavaScript! Python!), Et surtout: les différents compromis entre les threads et les coroutines. Pour certains cas d'utilisation, les coroutines sont objectivement meilleures.

L'un des plus grands paradigmes de programmation des années 80, 90 et aujourd'hui est la POO. Si nous regardons l'histoire de la POO et plus particulièrement le développement du langage Simula, nous voyons que les classes ont évolué à partir des coroutines. Simula était destiné à la simulation de systèmes avec des événements discrets. Chaque élément du système était un processus distinct qui s'exécutait en réponse aux événements pendant la durée d'une étape de simulation, puis cédait pour laisser d'autres processus faire leur travail. Pendant le développement de Simula 67, le concept de classe a été introduit. Désormais, l'état persistant de la coroutine est stocké dans les membres de l'objet et les événements sont déclenchés en appelant une méthode. Pour plus de détails, pensez à lire l'article Le développement des langages SIMULA par Nygaard & Dahl.

Donc, dans une tournure amusante, nous utilisons des coroutines depuis le début, nous les appelions simplement des objets et une programmation événementielle.

En ce qui concerne le parallélisme, il existe deux types de langages: ceux qui ont un modèle de mémoire approprié et ceux qui n'en ont pas. Un modèle de mémoire traite de choses comme «Si j'écris dans une variable et que je lis dans cette variable dans un autre thread, est-ce que je vois l'ancienne ou la nouvelle valeur ou peut-être une valeur non valide? Que signifient «avant» et «après»? Quelles opérations sont garanties d'être atomiques? "

La création d'un bon modèle de mémoire est difficile, donc cet effort n'a tout simplement jamais été fait pour la plupart de ces langages open source dynamiques non spécifiés et définis par l'implémentation: Perl, JavaScript, Python, Ruby, PHP. Bien sûr, tous ces langages ont évolué bien au-delà des «scripts» pour lesquels ils ont été initialement conçus. Eh bien, certaines de ces langues ont une sorte de document de modèle de mémoire, mais celles-ci ne sont pas suffisantes. Au lieu de cela, nous avons des hacks:

  • Perl peut être compilé avec la prise en charge des threads, mais chaque thread contient un clone distinct de l'état complet de l'interpréteur, ce qui rend les threads d'un coût prohibitif. Comme seul avantage, cette approche de partage de rien évite les courses de données et oblige les programmeurs à communiquer uniquement via les files d'attente / signaux / IPC. Perl n'a pas une histoire solide pour le traitement asynchrone.

  • JavaScript a toujours eu un support riche pour la programmation fonctionnelle, donc les programmeurs encodaient manuellement les continuations / rappels dans leurs programmes là où ils avaient besoin d'opérations asynchrones. Par exemple, avec des requêtes Ajax ou des retards d'animation. Comme le Web est intrinsèquement asynchrone, il y a beaucoup de code JavaScript asynchrone et la gestion de tous ces rappels est extrêmement douloureuse. Nous voyons donc de nombreux efforts pour mieux organiser ces rappels (promesses) ou pour les éliminer complètement.

  • Python a cette malheureuse fonctionnalité appelée Global Interpreter Lock. Fondamentalement, le modèle de mémoire Python est «Tous les effets apparaissent séquentiellement car il n'y a pas de parallélisme. Un seul thread exécutera du code Python à la fois. »Ainsi, bien que Python ait des threads, ceux-ci sont simplement aussi puissants que les coroutines. [1] Python peut coder de nombreuses coroutines via des fonctions de générateur avec yield. S'il est utilisé correctement, cela seul peut éviter la plupart des enfers de rappel connus de JavaScript. Le système async / wait plus récent de Python 3.5 rend les idiomes asynchrones plus pratiques en Python et intègre une boucle d'événement.

    [1]: Techniquement, ces restrictions ne s'appliquent qu'à CPython, l'implémentation de référence Python. D'autres implémentations comme Jython offrent de vrais threads qui peuvent s'exécuter en parallèle, mais doivent parcourir une grande longueur pour implémenter un comportement équivalent. Essentiellement: chaque variable ou membre d'objet est une variable volatile de sorte que toutes les modifications sont atomiques et immédiatement visibles dans tous les threads. Bien sûr, l'utilisation de variables volatiles est beaucoup plus coûteuse que l'utilisation de variables normales.

  • Je ne connais pas assez Ruby et PHP pour les rôtir correctement.

Pour résumer: certains de ces langages ont des décisions de conception fondamentales qui rendent le multithreading indésirable ou impossible, conduisant à une concentration plus forte sur des alternatives comme les coroutines et sur les moyens de rendre la programmation asynchrone plus pratique.

Enfin, parlons des différences entre coroutines et threads:

Les threads sont essentiellement comme des processus, sauf que plusieurs threads à l'intérieur d'un processus partagent un espace mémoire. Cela signifie que les threads ne sont en aucun cas «légers» en termes de mémoire. Les threads sont planifiés de manière préventive par le système d'exploitation. Cela signifie que les commutateurs de tâches ont une surcharge élevée et peuvent se produire à des moments peu pratiques. Cette surcharge a deux composantes: le coût de la suspension de l'état du thread et le coût de la commutation entre le mode utilisateur (pour le thread) et le mode noyau (pour le planificateur).

Si un processus planifie ses propres threads directement et en coopération, le changement de contexte en mode noyau n'est pas nécessaire et les tâches de commutation sont comparativement coûteuses par rapport à un appel de fonction indirect, comme dans: assez bon marché. Ces fils légers peuvent être appelés fils verts, fibres ou coroutines selon divers détails. Les utilisateurs notables de fils / fibres verts ont été les premières implémentations de Java, et plus récemment les Goroutines à Golang. Un avantage conceptuel des coroutines est que leur exécution peut être comprise en termes de flux de contrôle passant explicitement dans les deux sens entre les coroutines. Cependant, ces coroutines n'atteignent pas le véritable parallélisme à moins qu'elles ne soient planifiées sur plusieurs threads du système d'exploitation.

Où les coroutines bon marché sont-elles utiles? La plupart des logiciels n'ont pas besoin d'un thread gazillion, donc les threads chers normaux sont généralement OK. Cependant, la programmation asynchrone peut parfois simplifier votre code. Pour être utilisée librement, cette abstraction doit être suffisamment bon marché.

Et puis il y a le web. Comme mentionné ci-dessus, le Web est intrinsèquement asynchrone. Les demandes de réseau prennent simplement beaucoup de temps. De nombreux serveurs Web maintiennent un pool de threads plein de threads de travail. Cependant, la plupart du temps, ces threads seront inactifs car ils attendent une ressource, que ce soit en attendant un événement d'E / S lors du chargement d'un fichier à partir du disque, en attendant que le client ait accusé réception d'une partie de la réponse ou en attendant qu'une base de données la requête se termine. NodeJS a démontré de façon phénoménale qu'une conception de serveur basée sur les événements et asynchrone qui en résulte fonctionne extrêmement bien. Évidemment, JavaScript est loin d'être le seul langage utilisé pour les applications Web, il y a donc également une grande incitation pour les autres langages (perceptibles en Python et C #) à faciliter la programmation Web asynchrone.

amon
la source
Je recommanderais de vous procurer votre quatrième au dernier paragraphe pour éviter tout risque de plagiat, c'est presque exactement la même chose qu'une autre source que j'ai lue. De plus, tout en ayant des ordres de grandeur plus faibles que les threads, les performances des coroutines ne peuvent pas être simplifiées en "un appel de fonction indirecte". Voir Boosts détails sur la mise en œuvre de la coroutine ici et ici .
2017
1
@snb Concernant votre modification suggérée: le GIL peut être un détail d'implémentation CPython, mais le problème fondamental est que le langage Python n'a pas de modèle de mémoire explicite qui spécifie la mutation parallèle des données. Le GIL est un hack pour contourner ces problèmes. Mais les implémentations Python avec un vrai parallélisme doivent parcourir de grandes longueurs pour fournir une sémantique équivalente, par exemple comme discuté dans le livre Jython . Fondamentalement: chaque variable ou champ d'objet doit être une variable volatile coûteuse .
amon
3
@snb Concernant le plagiat: le plagiat présente faussement des idées comme les vôtres, en particulier dans un contexte académique. C'est une allégation sérieuse , mais je suis sûr que vous ne le pensiez pas comme ça. Le paragraphe «Les threads sont fondamentalement comme des processus» ne fait que réitérer des faits bien connus comme cela est enseigné dans toute conférence ou livre de texte sur les systèmes d'exploitation. Puisqu'il n'y a que de nombreuses façons de formuler ces faits de manière concise, je ne suis pas surpris que le paragraphe vous soit familier.
amon
Je n'ai pas changé le sens pour impliquer que Python avait un modèle de mémoire. De plus, l'utilisation de volatile ne diminue pas à elle seule les performances volatiles signifie simplement que le compilateur ne peut pas optimiser la variable de manière à supposer que la variable sera inchangée sans opérations explicites dans le contexte actuel. Dans le monde Jython, cela pourrait réellement avoir de l'importance, car il va utiliser la compilation VM JIT, mais dans le monde CPython, vous ne vous inquiétez pas de l'optimisation JIT, vos variables volatiles existeraient dans l'espace d'exécution de l'interpréteur, où aucune optimisation ne pourrait être effectuée .
2017
7

Les coroutines étaient utiles parce que les systèmes d'exploitation n'effectuaient pas de planification préventive . Une fois qu'ils ont commencé à fournir une planification préventive, il était plus nécessaire de renoncer périodiquement au contrôle de votre programme.

Au fur et à mesure que les processeurs multicœurs deviennent plus répandus, les coroutines sont utilisées pour réaliser le parallélisme des tâches et / ou maintenir une utilisation élevée du système (lorsqu'un thread d'exécution doit attendre une ressource, un autre peut commencer à s'exécuter à sa place).

NodeJS est un cas spécial, où des coroutines sont utilisées pour obtenir un accès parallèle à IO. Autrement dit, plusieurs threads sont utilisés pour traiter les demandes d'E / S, mais un seul thread est utilisé pour exécuter le code javascript. L'exécution d'un code utilisateur dans un thread de signalisation a pour but d'éviter la nécessité d'utiliser des mutex. Cela entre dans la catégorie des tentatives de maintenir une utilisation élevée du système comme mentionné ci-dessus.

dlasalle
la source
4
Mais les coroutines ne sont pas gérées par le système d'exploitation. OS ne sait pas ce qu'est une coroutine, contrairement aux fibres C ++
Surexchange
De nombreux systèmes d'exploitation ont des coroutines.
Jörg W Mittag
les coroutines comme python et Javascript ES6 + ne sont-elles pas multiprocessus? Comment ceux-ci parviennent-ils au parallélisme des tâches?
quand le
1
@Mael Le récent "renouveau" des coroutines vient du python et du javascript, qui n'atteignent pas le parallélisme avec leurs coroutines si je comprends bien. C'est-à-dire que cette réponse est incorrecte, car le parrallisme de tâche n'est pas du tout la raison pour laquelle les coroutines sont "de retour". Luas n'est pas non plus multiprocessus? EDIT: Je viens de réaliser que vous ne parliez pas de parallélisme, mais pourquoi m'avez-vous répondu en premier lieu? Répondez à dlasalle, car ils ont clairement tort.
quand le
3
@dlasalle Non, ils ne le peuvent pas malgré le fait qu'il indique "s'exécuter en parallèle", ce qui ne signifie pas qu'un code est exécuté physiquement en même temps. GIL l'arrêterait et async ne génèrerait pas de processus séparés requis pour le multitraitement dans CPython (GILs séparés). Async fonctionne avec des rendements sur un seul thread. Quand ils disent "parallèle", ils signifient en fait que plusieurs fonctions se relient à d'autres fonctions et exécutent l'exécution des fonctions. Les processus asynchrones Python ne peuvent pas être exécutés en parallèle à cause de l'impl. J'ai maintenant trois langues qui ne font pas de coroutines parallèles, Lua, Javascript et Python.
quand le
5

Les premiers systèmes utilisaient les coroutines pour fournir la concurrence principalement parce qu'ils sont la manière la plus simple de le faire. Les threads nécessitent une bonne quantité de support du système d'exploitation (vous pouvez les implémenter au niveau de l'utilisateur, mais vous aurez besoin d'un moyen d'arranger périodiquement le système pour interrompre votre processus) et sont plus difficiles à implémenter même lorsque vous avez le support .

Les threads ont commencé à prendre le relais plus tard car, dans les années 70 ou 80, tous les systèmes d'exploitation sérieux les prenaient en charge (et, dans les années 90, même Windows!), Et ils sont plus généraux. Et ils sont plus faciles à utiliser. Tout à coup, tout le monde pensait que les fils étaient la prochaine grande chose.

À la fin des années 90, des fissures commençaient à apparaître, et au début des années 2000, il est devenu évident qu'il y avait de graves problèmes avec les fils:

  1. ils consomment beaucoup de ressources
  2. les changements de contexte prennent beaucoup de temps, relativement parlant, et sont souvent inutiles
  3. ils détruisent la localité de référence
  4. écrire un code correct qui coordonne plusieurs ressources pouvant nécessiter un accès exclusif est d'une difficulté inattendue

Au fil du temps, le nombre de tâches que les programmes doivent généralement effectuer à tout moment a augmenté rapidement, ce qui a accru les problèmes causés par (1) et (2) ci-dessus. La disparité entre la vitesse du processeur et les temps d'accès à la mémoire a augmenté, aggravant le problème (3). Et la complexité des programmes en termes de nombre et de types de ressources dont ils ont besoin a augmenté, augmentant la pertinence du problème (4).

Mais en perdant un peu de généralité et en obligeant un peu plus le programmeur à réfléchir à la façon dont leurs processus peuvent fonctionner ensemble, les coroutines peuvent résoudre tous ces problèmes.

  1. Les coroutines nécessitent un peu plus de ressources qu'une poignée de pages pour la pile, beaucoup moins que la plupart des implémentations de threads.
  2. Les coroutines ne changent de contexte qu'aux points définis par le programmeur, ce qui, espérons-le, ne signifie que lorsque cela est nécessaire. De plus, ils n'ont généralement pas besoin de conserver autant d'informations de contexte (par exemple, les valeurs de registre) que les threads, ce qui signifie que chaque commutateur est généralement plus rapide et en nécessite moins.
  3. Les modèles de coroutines courants, y compris les opérations de type producteur / consommateur, transfèrent les données entre les routines d'une manière qui augmente activement la localité. En outre, les changements de contexte ne se produisent généralement qu'entre des unités de travail ne se trouvant pas à l'intérieur d'elles, c'est-à-dire à un moment où la localité est généralement minimisée de toute façon.
  4. Le verrouillage des ressources est moins susceptible d'être nécessaire lorsque les routines savent qu'elles ne peuvent pas être interrompues arbitrairement au milieu d'une opération, ce qui permet aux implémentations plus simples de fonctionner correctement.
Jules
la source
5

Préface

Je veux commencer par énoncer une raison pour laquelle les coroutines n'obtiennent pas une résurgence, le parallélisme. En général, les coroutines modernes ne sont pas un moyen de réaliser un parallélisme basé sur les tâches, car les implémentations modernes n'utilisent pas la fonctionnalité de multitraitement. Ce qui se rapproche le plus, ce sont des choses comme les fibres .

Utilisation moderne (pourquoi ils sont de retour)

Les coroutines modernes sont venues comme un moyen d'obtenir une évaluation paresseuse , quelque chose de très utile dans les langages fonctionnels comme haskell, où au lieu d'itérer sur un ensemble entier pour effectuer une opération, vous seriez en mesure d'effectuer une évaluation d'opération uniquement autant que nécessaire ( utile pour des ensembles infinis d'articles ou autrement grands ensembles avec résiliation anticipée et sous-ensembles).

Avec l'utilisation du mot-clé Yield pour créer des générateurs (qui en eux-mêmes satisfont une partie des besoins d'évaluation paresseuse) dans des langages comme Python et C #, les coroutines, dans l'implémentation moderne étaient non seulement possibles, mais possibles sans syntaxe spéciale dans le langage lui-même (bien que python ait finalement ajouté quelques bits pour aider). Co-routines avec l' aide evaulation paresseux avec l'idée de l' avenir de où , si vous n'avez pas besoin de la valeur d'une variable à ce moment - là, vous pouvez retarder l' acquisition de fait jusqu'à ce que vous demandez explicitement cette valeur (vous permettant d'utiliser la valeur et l'évaluer paresseusement à un moment différent de l'instanciation).

Au-delà de l'évaluation paresseuse, cependant, en particulier dans la sphère Web, ces routines co aident à résoudre l' enfer de rappel . Les coroutines deviennent utiles dans l'accès aux bases de données, les transactions en ligne, l'interface utilisateur, etc., où le temps de traitement sur la machine cliente elle-même n'entraînera pas un accès plus rapide à ce dont vous avez besoin. Le filetage pourrait remplir la même chose, mais nécessite beaucoup plus de frais généraux dans ce domaine, et contrairement aux coroutines, ils sont en fait utiles pour le parallélisme des tâches .

En bref, à mesure que le développement Web se développe et que les paradigmes fonctionnels fusionnent davantage avec les langages impératifs, les coroutines sont devenues une solution aux problèmes asynchrones et à l'évaluation paresseuse. Les coroutines arrivent dans des espaces problématiques où le filetage multiprocessus et le filetage en général sont soit inutiles, incommodes ou impossibles.

Exemple moderne

Les coroutines dans des langages comme Javascript, Lua, C # et Python dérivent toutes leurs implémentations par des fonctions individuelles abandonnant le contrôle du thread principal à d'autres fonctions (rien à voir avec les appels du système d'exploitation).

Dans cet exemple de python , nous avons une fonction python amusante avec quelque chose appelé à l' awaitintérieur. Il s'agit essentiellement d'un rendement, qui renvoie l'exécution à ce loopqui permet ensuite à une fonction différente de s'exécuter (dans ce cas, une factorialfonction différente ). Notez que quand il dit "exécution parallèle de tâches" qui est un terme impropre, il ne s'exécute pas réellement en parallèle, l'exécution de sa fonction d' entrelacement en utilisant le mot-clé attendent (qui gardent à l'esprit est juste un type spécial de rendement)

Ils permettent des rendements de contrôle uniques et non parallèles pour des processus simultanés qui ne sont pas parallèles aux tâches , dans le sens où ces tâches ne fonctionnent jamais en même temps. Les coroutines ne sont pas des threads dans les implémentations de langage moderne. Toutes ces implémentations de langages de routines de co sont dérivées de ces appels de rendement de fonction (que vous, le programmeur, devez réellement insérer manuellement dans vos routines de co).

EDIT: C ++ Boost coroutine2 fonctionne de la même manière, et leur explication devrait donner une meilleure vision de ce dont je parle avec yeilds, voir ici . Comme vous pouvez le voir, il n'y a pas de "cas spécial" avec les implémentations, des choses comme les fibres boost sont l'exception à la règle, et même alors nécessitent une synchronisation explicite.

EDIT2: puisque quelqu'un pensait que je parlais d'un système basé sur les tâches c #, je ne l'étais pas. Je parlais du système Unity et des implémentations c # naïves

quand
la source
@ T.Sar Je n'ai jamais dit que C # avait des coroutines "naturelles", ni C ++ (pourrait changer) ni python (et il les avait toujours), et tous les trois ont des implémentations de routine. Mais toutes les implémentations C # des coroutines (comme celles en unité) sont basées sur le rendement comme je le décris. De plus, votre utilisation de "hack" ici n'a aucun sens, je suppose que chaque programme est un hack car il n'a pas toujours été défini dans le langage. Je ne mélange aucunement le "système basé sur les tâches" C # avec quoi que ce soit, je ne l'ai même pas mentionné.
2017
Je suggérerais de rendre votre réponse un peu plus claire. C # a à la fois le concept d'instructions d'attente et un système de parallélisme basé sur les tâches - utiliser C # et ces mots tout en donnant des exemples sur python sur la façon dont python n'est pas vraiment parallèle peut provoquer beaucoup de confusion. Supprimez également votre première phrase - il n'est pas nécessaire d'attaquer directement les autres utilisateurs dans une réponse comme celle-ci.
T. Sar - Rétablir Monica