web-dev-qa-db-fra.com

Différence entre une "coroutine" et un "fil"?

Quelles sont les différences entre une "coroutine" et un "fil"?

129
jldupont

Les coroutines sont une forme de traitement séquentiel: une seule s'exécute à un moment donné (tout comme les sous-routines procédures AKA, les fonctions AKA - elles passent simplement le relais entre elles de manière plus fluide).

Les threads sont (au moins conceptuellement) une forme de traitement simultané: plusieurs threads peuvent être exécutés à un moment donné. (Traditionnellement, sur les machines à processeur unique et à cœur unique, cette concurrence a été simulée avec l'aide du système d'exploitation - de nos jours, étant donné que de nombreuses machines sont multi-CPU et/ou multi-core, les threads de facto être exécuté simultanément, pas seulement "conceptuellement").

96
Alex Martelli

Première lecture: Concurrence vs Parallélisme - Quelle est la différence?

La concurrence est la séparation des tâches pour fournir une exécution entrelacée. Le parallélisme est l'exécution simultanée de plusieurs travaux afin d'augmenter la vitesse. — https://github.com/servo/servo/wiki/Design

Réponse courte: Avec les threads, le système d'exploitation commute les threads en cours d'exécution de manière préventive en fonction de son ordonnanceur, qui est un algorithme dans le noyau du système d'exploitation. Avec les coroutines, le programmeur et le langage de programmation déterminent quand changer de coroutine; en d'autres termes, les tâches sont multitâches en coopération en mettant en pause et en reprenant des fonctions à des points de consigne, généralement (mais pas nécessairement) dans un seul thread.

Réponse longue: Contrairement aux threads, qui sont planifiés de manière préventive par le système d'exploitation, les commutateurs coroutine sont coopératifs, c'est-à-dire le programmeur (et éventuellement la programmation et son runtime) contrôle quand un changement se produira.

Contrairement aux threads, qui sont préventifs, les commutateurs coroutine sont coopératifs (le programmeur contrôle quand un commutateur se produira). Le noyau n'est pas impliqué dans les commutateurs coroutine. — http://www.boost.org/doc/libs/1_55_0/libs/coroutine/doc/html/coroutine/overview.html

Un langage qui prend en charge les threads natifs peut exécuter ses threads (threads utilisateur) sur les threads du système d'exploitation ( threads du noyau ). Chaque processus a au moins un thread noyau. Les threads du noyau sont comme des processus, sauf qu'ils partagent l'espace mémoire dans leur processus propriétaire avec tous les autres threads de ce processus. Un processus "possède" toutes ses ressources affectées, comme la mémoire, les descripteurs de fichiers, les sockets, les descripteurs de périphérique, etc., et ces ressources sont toutes partagées entre ses threads du noyau.

Le planificateur du système d'exploitation fait partie du noyau qui exécute chaque thread pendant un certain temps (sur une machine à processeur unique). Le planificateur alloue du temps (découpage en temps) à chaque thread, et si le thread n'est pas terminé dans ce délai, le planificateur le préempte (l'interrompt et passe à un autre thread). Plusieurs threads peuvent s'exécuter en parallèle sur une machine multiprocesseur, car chaque thread peut être (mais ne doit pas nécessairement être) planifié sur un processeur séparé.

Sur une machine à processeur unique, les threads sont découpés en temps et préemptés (basculés entre) rapidement (sous Linux, la tranche de temps par défaut est de 100 ms), ce qui les rend simultanés. Cependant, ils ne peuvent pas être exécutés en parallèle (simultanément), car un processeur monocœur ne peut exécuter qu'une seule chose à la fois.

Les générateurs Coroutines et/ou peuvent être utilisés pour implémenter des fonctions coopératives. Au lieu d'être exécutés sur des threads du noyau et planifiés par le système d'exploitation, ils s'exécutent dans un seul thread jusqu'à ce qu'ils cèdent ou se terminent, cédant à d'autres fonctions déterminées par le programmeur. Les langages avec générateurs , tels que Python et ECMAScript 6, peuvent être utilisés pour construire des coroutines. Async/wait (vu en C #, Python, ECMAscript 7, Rust) est une abstraction construite au-dessus des fonctions du générateur qui produisent des futures/promesses.

Dans certains contextes, les coroutines peuvent faire référence à des fonctions empilables tandis que les générateurs peuvent faire référence aux fonctions sans pile.

Fibres , fils légers , et les fils verts sont d'autres noms pour les coroutines ou les choses semblables à des coroutines. Ils peuvent parfois ressembler (généralement à dessein) à des threads de système d'exploitation dans le langage de programmation, mais ils ne fonctionnent pas en parallèle comme de vrais threads et fonctionnent à la place comme des coroutines. (Il peut y avoir des particularités techniques ou des différences plus spécifiques entre ces concepts selon la langue ou la mise en œuvre.)

Par exemple, Java avait " fils verts "; il s'agissait de fils planifiés par le Java machine virtuelle (JVM) au lieu de nativement sur les threads du noyau du système d'exploitation sous-jacent. Ceux-ci ne fonctionnaient pas en parallèle ou ne tiraient pas parti de plusieurs processeurs/cœurs - car cela nécessiterait un thread natif! Comme ils n'étaient pas planifiés par le système d'exploitation, ils ressemblaient plus à des coroutines qu'à des fils du noyau. Les fils verts sont ce que Java utilisé jusqu'à ce que les fils natifs soient introduits dans Java 1.2.

Les threads consomment des ressources. Dans la machine virtuelle Java, chaque thread a sa propre pile, généralement de 1 Mo. 64 Ko est le moins d'espace de pile autorisé par thread dans la machine virtuelle Java. La taille de la pile de threads peut être configurée sur la ligne de commande de la JVM. Malgré le nom, les threads ne sont pas gratuits, en raison de leurs ressources d'utilisation comme chaque thread ayant besoin de sa propre pile, du stockage local des threads (le cas échéant) et du coût de la programmation des threads/changement de contexte/invalidation du cache du processeur. C'est en partie la raison pour laquelle les coroutines sont devenues populaires pour les applications hautement concurrentes à performances critiques.

Mac OS n'autorisera qu'un processus à allouer environ 2000 threads, et Linux allouera une pile de 8 Mo par thread et n'autorisera que le nombre de threads pouvant tenir dans la RAM physique.

Par conséquent, les threads sont les plus lourds (en termes d'utilisation de la mémoire et de temps de changement de contexte), puis les coroutines et enfin les générateurs sont les plus légers.

143
llambda

Environ 7 ans de retard, mais les réponses ici manquent de contexte sur les co-routines vs les threads. Pourquoi coroutines reçoit-il tant d'attention ces derniers temps, et quand les utiliserais-je par rapport à threads?

Tout d'abord, si les coroutines s'exécutent simultanément (jamais dans parallèle), pourquoi est-ce que quelqu'un les préférerait aux threads?

La réponse est que les coroutines peuvent fournir un très haut niveau de concurrence avec très peu de frais généraux . Généralement, dans un environnement threadé, vous avez au plus 30 à 50 threads avant que la quantité de surcharge gaspillée ne planifie réellement ces threads (par le planificateur système) de manière significative la durée pendant laquelle les threads effectuent un travail utile.

Ok donc avec des threads vous pouvez avoir du parallélisme, mais pas trop de parallélisme, n'est-ce pas encore mieux qu'une co-routine s'exécutant sur un seul thread? Enfin pas forcément. N'oubliez pas qu'une co-routine peut toujours faire de la concurrence sans surcharge du planificateur - elle gère simplement le changement de contexte lui-même.

Par exemple, si vous avez une routine qui effectue un certain travail et qu'elle exécute une opération que vous savez bloquée pendant un certain temps (c'est-à-dire une requête réseau), avec une co-routine, vous pouvez immédiatement basculer vers une autre routine sans avoir à surcharger le planificateur système dans cette décision - oui vous le programmeur doit spécifier quand les co-routines peuvent changer.

Avec de nombreuses routines qui effectuent de très petits travaux et changent volontairement entre elles, vous avez atteint un niveau d'efficacité qu'aucun programmateur ne pourrait espérer atteindre. Vous pouvez maintenant avoir des milliers de coroutines travaillant ensemble, par opposition à des dizaines de threads.

Parce que vos routines basculent désormais entre elles un point prédéterminé, vous pouvez désormais aussi éviter de verrouiller sur les structures de données partagées (parce que vous ne diriez jamais à votre code de passer à une autre coroutine au milieu d'une section critique )

Un autre avantage est l'utilisation de mémoire beaucoup plus faible. Avec le modèle threadé, chaque thread doit allouer sa propre pile, et donc votre utilisation de la mémoire croît linéairement avec le nombre de threads que vous avez. Avec les co-routines, le nombre de routines que vous avez n'a pas de relation directe avec votre utilisation de la mémoire.

Et enfin, les co-routines reçoivent beaucoup d'attention car dans certains langages de programmation (tels que Python), vos threads ne peuvent pas fonctionner en parallèle de toute façon - ils s'exécutent simultanément, tout comme les coroutines, mais sans la faible mémoire et la surcharge de planification gratuite.

86
Martin Konecny

En un mot: la préemption. Les coroutines agissent comme des jongleurs qui se transmettent des points bien répétés. Les threads (vrais threads) peuvent être interrompus à presque n'importe quel moment, puis repris plus tard. Bien sûr, cela entraîne toutes sortes de problèmes de conflit de ressources, d'où l'infâme GIL - Global Interpreter Lock de Python.

De nombreuses implémentations de threads ressemblent en fait davantage à des coroutines.

18
Peter Rowell

Cela dépend de la langue que vous utilisez. Par exemple dans Lua c'est la même chose (le type de variable d'une coroutine s'appelle thread).

Habituellement, bien que les coroutines implémentent un rendement volontaire où (vous) le programmeur décide où yield, c'est-à-dire, donne le contrôle à une autre routine.

Les threads sont plutôt gérés automatiquement (arrêtés et démarrés) par le système d'exploitation, et ils peuvent même s'exécuter en même temps sur des processeurs multicœurs.

9
Thomas Bonini