Différence entre une «coroutine» et un «fil»?


Réponses:


122

Les coroutines sont une forme de traitement séquentiel: une seule est en cours d'exécution à un moment donné (tout comme les sous-programmes AKA procédures AKA fonctions - ils passent juste le témoin entre eux de manière plus fluide).

Les threads sont (au moins conceptuellement) une forme de traitement simultané: plusieurs threads peuvent être en cours d'exécution à tout moment. (Traditionnellement, sur les machines à processeur unique et à cœur unique, cette concurrence était simulée avec l'aide du système d'exploitation - de nos jours, étant donné que de nombreuses machines sont multi-processeurs et / ou multi-cœur, les threads s'exécuteront de facto simultanément, pas seulement «conceptuellement»).


188

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 change les threads en cours d'exécution de manière préventive en fonction de son planificateur, qui est un algorithme du noyau du système d'exploitation. Avec les coroutines, le programmeur et le langage de programmation déterminent quand changer de coroutines; en d'autres termes, les tâches sont multitâches de manière coopérative en mettant en pause et en reprenant les fonctions à des points de consigne, généralement (mais pas nécessairement) dans un seul thread.

Réponse longue: contrairement aux threads, qui sont programmés de manière préventive par le système d'exploitation, les commutateurs coroutine sont coopératifs, ce qui signifie que le programmeur (et éventuellement le langage de programmation et son exécution) contrôle le moment où un changement se produira.

Contrairement aux threads, qui sont préemptifs, les commutateurs coroutine sont coopératifs (le programmeur contrôle le moment où un commutateur se produira). Le noyau n'est pas impliqué dans les commutateurs de 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 de noyau. Les threads du noyau sont comme des processus, sauf qu'ils partagent l'espace mémoire de leur propre processus 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ériques, 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 (timelicing) à 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 pas nécessairement) planifié sur un processeur séparé.

Sur une machine à processeur unique, les threads sont découpés dans le temps et préemptés (commuté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.

Des coroutines et / ou des générateurs 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 des générateurs , tels que Python et ECMAScript 6, peuvent être utilisés pour créer des coroutines. Async / await (vu en C #, Python, ECMAscript 7, Rust) est une abstraction construite au-dessus des fonctions génératrices qui donnent des futurs / promesses.

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

Les fibres , les fils légers et les fils verts sont d'autres noms pour les coroutines ou les éléments de type coroutine. Ils peuvent parfois ressembler (généralement volontairement) à des threads du système d'exploitation dans le langage de programmation, mais ils ne fonctionnent pas en parallèle comme de vrais threads et fonctionnent plutôt comme des coroutines. (Il peut y avoir des particularités techniques plus spécifiques ou des différences entre ces concepts en fonction du langage ou de la mise en œuvre.)

Par exemple, Java avait des " threads verts "; il s'agissait de threads qui étaient planifiés par la machine virtuelle Java (JVM) au lieu d'être 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 programmés par le système d'exploitation, ils ressemblaient plus à des coroutines qu'à des threads du noyau. Les threads verts sont ce que Java a utilisé jusqu'à ce que les threads natifs soient introduits dans Java 1.2.

Les threads consomment des ressources. Dans la JVM, chaque thread a sa propre pile, généralement de 1 Mo. 64 Ko est la plus petite quantité d'espace de pile autorisée par thread dans la JVM. 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 nécessitant sa propre pile, le stockage local des threads (le cas échéant) et le coût de la planification des threads / du changement de contexte / de l'invalidation du cache du processeur. C'est en partie la raison pour laquelle les coroutines sont devenues populaires pour les applications critiques en termes de performances et hautement simultanées.

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

Par conséquent, les threads sont le poids le plus lourd (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.


2
+1, mais cette réponse pourrait bénéficier de quelques références.
kojiro

1
Les fils verts sont quelque chose de différent des coroutines. n'est-ce pas? Même les fibres présentent certaines différences. voir programmers.stackexchange.com/questions/254140/…

113

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

Tout d'abord, si les coroutines s'exécutent simultanément (jamais en parallèle ), pourquoi quelqu'un les préférerait-il 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 en planifiant réellement ces threads (par le planificateur système) réduit considérablement le temps pendant lequel les threads effectuent un travail utile.

Ok donc avec les threads, vous pouvez avoir un parallélisme, mais pas trop de parallélisme, n'est-ce pas encore mieux qu'une co-routine exécutée dans 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 effectue une opération que vous savez qu'elle bloquera pendant un certain temps (c'est-à-dire une requête réseau), avec une co-routine, vous pouvez immédiatement passer à une autre routine sans avoir à inclure le planificateur système dans cette décision - oui, vous, le programmeur, devez spécifier quand les co-routines peuvent changer.

Avec de nombreuses routines effectuant de très petits travaux et basculant volontairement entre elles, vous avez atteint un niveau d'efficacité qu'aucun planificateur ne pourrait espérer atteindre. Vous pouvez maintenant avoir des milliers de coroutines travaillant ensemble au lieu de dizaines de threads.

Parce que vos routines basculent maintenant entre elles sur des points prédéterminés, vous pouvez désormais également éviter de verrouiller des structures de données partagées (car vous ne diriez jamais à votre code de passer à une autre coroutine au milieu d'une section critique)

Un autre avantage est l'utilisation beaucoup plus faible de la mémoire. Avec le modèle de thread, chaque thread doit allouer sa propre pile, et donc votre utilisation de la mémoire augmente 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 mémoire insuffisante et la surcharge de planification gratuite.


2
Comment faire un passage à une autre tâche dans les coroutines lorsque nous rencontrons une opération de blocage?
Narcisse Doudieu Siewe

La façon dont vous passez à une autre tâche consiste à effectuer une opération de blocage de manière asynchrone. Cela signifie que vous devez éviter d'utiliser toute opération qui bloquerait réellement, et n'utiliser que des opérations qui prennent en charge le non-blocage lorsqu'elles sont utilisées dans votre système coroutine. Le seul moyen de contourner cela est d'avoir des coroutines supportées par le noyau, comme UMS sur Windows par exemple, où elles sautent dans votre planificateur chaque fois que votre "thread" UMS se bloque sur un appel système.
retep998

@MartinKonecny ​​Est-ce que le TS C ++ Threads récent adhère à l'approche que vous avez mentionnée?
Nikos

Donc, finalement, un langage de programmation moderne aurait besoin à la fois de Coroutines / Fibres pour utiliser efficacement un seul cœur de processeur, par exemple pour des opérations non lourdes de calcul comme IO et Threads afin de paralléliser les opérations gourmandes en processeur sur de nombreux cœurs pour gagner en vitesse, n'est-ce pas?
Mahatma_Fatal_Error

19

En un mot: la préemption. Les coroutines agissent comme des jongleurs qui ne cessent de se transmettre des points bien préparé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 conflits de ressources, d'où le tristement célèbre GIL - Global Interpreter Lock de Python.

De nombreuses implémentations de thread ressemblent plus à des coroutines.


9

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

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

À la place, les threads sont automatiquement gérés (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.


0

12 ans de retard à la discussion mais une coroutine a l'explication dans le nom. Coroutine peut être décomposée en Co et Routine.

Une routine dans ce contexte est juste une séquence d'opérations / actions et en exécutant / traitant une routine, la séquence d'opérations est exécutée une par une dans exactement le même ordre que spécifié.

Co est synonyme de coopération. Il est demandé à une co-routine de suspendre volontairement son exécution (ou mieux de s'y attendre) pour donner à d'autres co-routines une chance de s'exécuter également. Ainsi, une co-routine consiste à partager les ressources CPU (volontairement) afin que d'autres puissent utiliser la même ressource que celle que vous utilisez.

Un thread en revanche n'a pas besoin de suspendre son exécution. Être suspendu est complètement transparent pour le thread et le thread est forcé par le matériel sous-jacent de se suspendre. Cela est également fait de manière à ce qu'il soit principalement transparent pour le thread car il n'est pas notifié et son état n'est pas modifié mais enregistré et restauré plus tard lorsque le thread est autorisé à continuer.

Une chose qui n'est pas vraie, c'est que les co-routines ne peuvent pas être exécutées simultanément et que les conditions de concurrence ne peuvent pas se produire. Cela dépend du système sur lequel les co-routines sont exécutées et il est facile d'imaginer les co-routines.

Peu importe comment les co-routines se suspendent. De retour dans Windows 3.1, int 03 était intégré à n'importe quel programme (ou devait y être placé) et en C #, nous ajoutions le rendement.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.