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?
python
multithreading
concurrency
multitasking
user1787812
la source
la source
Réponses:
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.
la source
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.
la source
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:
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.
la source
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'
await
intérieur. Il s'agit essentiellement d'un rendement, qui renvoie l'exécution à celoop
qui permet ensuite à une fonction différente de s'exécuter (dans ce cas, unefactorial
fonction 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
la source