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

Quelles sont les différences entre une “coroutine” et un “thread”?

Les coroutines sont une forme de traitement séquentiel: une seule est exécutée à un moment donné (tout comme les procédures AKA des sous-programmes, les fonctions AKA – elles ne font que passer 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 à tout moment. (Traditionnellement, sur les machines à un seul processeur et à cœur unique, cette simultanéité était simulée avec l’aide du système d’exploitation). pas seulement “conceptuellement”).

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 pièces de travail afin d’augmenter la vitesse. – https://github.com/servo/servo/wiki/Design

Réponse courte: Avec les threads, le système d’exploitation bascule les threads en cours de manière préventive en fonction de son ordonnanceur, qui est un algorithme dans le kernel 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 effectuées en mode multitâche 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, programmés de manière préventive par le système d’exploitation, les commutateurs de coroutine sont coopératifs, ce qui signifie que le programmeur (et éventuellement le langage de programmation et son environnement d’exécution) contrôle le moment où un changement se produira.

Contrairement aux threads, qui sont préemptifs, les commutateurs de coroutine sont coopératifs (le programmeur contrôle quand un changement se produira). Le kernel 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 kernel ). Chaque processus a au moins un thread de kernel. Les threads du kernel 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 les ressources qui lui sont assignées, telles que la mémoire, les descripteurs de fichiers, les sockets, les descripteurs de périphériques, etc., et ces ressources sont toutes partagées entre les threads du kernel.

Le planificateur du système d’exploitation fait partie du kernel qui exécute chaque thread pendant un certain temps (sur un ordinateur à 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 bascule vers un autre thread). Plusieurs threads peuvent être exécutés en parallèle sur une machine multiprocesseur, car chaque thread peut être (mais ne doit pas nécessairement être) programmé sur un processeur distinct.

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

Des coroutines et / ou des générateurs peuvent être utilisés pour mettre en œuvre des fonctions coopératives. Au lieu d’être exécutés sur les threads du kernel et programmés par le système d’exploitation, ils s’exécutent dans un seul thread jusqu’à ce qu’ils finissent ou aboutissent, 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 / wait (vu dans C #, Python, ECMAscript 7, Rust) est une abstraction construite sur les fonctions générasortingces qui génèrent des futures / 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 stack.

Les fibres , les fils légers et les fils verts sont d’autres noms pour les coroutines ou les objects de type coroutine. Ils peuvent parfois ressembler (généralement à des fins délibérées) à des threads de système d’exploitation dans le langage de programmation, mais ils ne s’exécutent pas en parallèle comme de véritables 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 de la langue ou de la mise en œuvre.)

Par exemple, Java avait des ” threads verts “; Il s’agissait de threads programmés par la machine virtuelle Java (JVM) au lieu de natifs sur les threads du kernel du système d’exploitation sous-jacent. Celles-ci ne fonctionnaient pas en parallèle ou tiraient 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 kernel. Les threads verts sont ce que Java utilisait jusqu’à ce que les threads natifs soient introduits dans Java 1.2.

Les threads consumnt des ressources. Dans la machine virtuelle Java, chaque thread a sa propre stack, généralement d’une taille de 1 Mo. 64k est la quantité minimale d’espace de stack autorisée par thread dans la machine virtuelle Java. La taille de la stack de threads peut être configurée sur la ligne de commande de la JVM. Malgré le nom, les threads ne sont pas libres, en raison de leurs ressources d’utilisation comme chaque thread nécessitant sa propre stack, son stockage local (le cas échéant) et le coût de la planification des threads / du changement de contexte / de l’invalidation du cache CPU. C’est en partie la raison pour laquelle les coroutines sont devenues populaires pour les applications hautement critiques et à haute performance.

Mac OS n’autorisera qu’un processus à allouer environ 2000 threads, et Linux allouera une stack de 8 Mo par thread et n’autorisera que autant 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.

Environ 7 ans de retard, mais les réponses ici manquent de contexte sur les co-routines et les 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 les préfèreraient-elles 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 maximum 30 à 50 threads avant que le temps passé à planifier ces threads (par le planificateur système) ne réduise considérablement la durée de travail des threads.

Ok, donc avec les threads, vous pouvez avoir du 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? Eh bien pas forcément. Rappelez-vous 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 à effectuer et que vous effectuez une opération que vous savez bloquée pendant un certain temps (une requête réseau), avec une routine commune, vous pouvez immédiatement passer à une autre routine sans inclure le planificateur système. cette décision – oui vous le programmeur doit spécifier quand les co-routines peuvent changer.

Avec beaucoup de routines effectuant très peu de travail et en changeant volontairement les uns avec les autres, vous avez atteint un niveau d’efficacité que le programmateur ne peut jamais espérer atteindre. Vous pouvez maintenant avoir des milliers de coroutines travaillant ensemble, par opposition à des dizaines de threads.

Comme vos routines passent maintenant d’un point à l’autre, vous pouvez désormais éviter de verrouiller les structures de données partagées (car vous ne diriez jamais à votre code de basculer vers une autre coroutine au milieu d’une section critique).

Un autre avantage est l’utilisation de la mémoire beaucoup plus faible. Avec le modèle de thread, chaque thread doit allouer sa propre stack, 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 finalement, les co-routines reçoivent beaucoup d’attention car dans certains langages de programmation (tels que Python) vos threads ne peuvent pas s’exécuter de toute façon en parallèle – ils s’exécutent comme les coroutines, mais sans la mémoire insuffisante et la surcharge de planification libre.

En un mot: la préemption. Les coroutines agissent comme des jongleurs qui se transmettent sans cesse des points bien répétés. Les threads (vrais threads) peuvent être interrompus à presque n’importe quel point puis repris plus tard. Bien sûr, cela apporte toutes sortes de problèmes de conflit de ressources, d’où le fameux GIL – Global Interpreter Lock de Python.

De nombreuses implémentations de threads sont en fait plus comme des coroutines.

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

Habituellement, bien que les coroutines mettent en œuvre un rendement volontaire où (vous) le programmeur décide où yield , c.-à-d., 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 peuvent même s’exécuter simultanément sur des processeurs multicœurs.