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.