Pourquoi les fibres ne peuvent-elles pas utiliser plusieurs processeurs?

8

Il semble que la distinction entre les fibres et les fils est que les fibres sont programmées en coopération, tandis que les fils sont programmés de manière préventive. Le but de l'ordonnanceur semble être un moyen de faire en sorte qu'une ressource processeur autrement série agisse de manière parallèle, en "partageant le temps" avec le CPU. Cependant, sur un processeur double cœur avec chaque cœur exécutant son propre thread, je suppose qu'il n'est pas nécessaire de suspendre l'exécution d'un thread pour que l'autre continue car ils ne "partagent pas le temps" avec un seul processeur.

Donc, si la différence entre les threads et les fibres réside dans la façon dont ils sont interrompus par le planificateur, et que l'interruption n'est pas nécessaire lors de l'exécution sur des cœurs physiquement séparés, pourquoi les fibres ne peuvent-elles pas profiter de plusieurs cœurs de processeur lorsque les threads le peuvent?

Sources de confusion:

.. principalement wikipedia

  1. http://en.wikipedia.org/wiki/Fiber_%28computer_science%29

    Un inconvénient est que les fibres ne peuvent pas utiliser des machines multiprocesseurs sans utiliser également des threads préemptifs

  2. http://en.wikipedia.org/wiki/Computer_multitasking#Multithreading

    ... les fibres ont tendance à perdre tout ou partie des avantages des fils sur les machines à processeurs multiples.

James M. Lay
la source

Réponses:

9

La principale distinction, comme vous le signalez dans votre question, est de savoir si le planificateur préemptera jamais un thread. La façon dont un programmeur pense au partage des structures de données ou à la synchronisation entre les «threads» est très différente dans les systèmes préemptifs et coopératifs.

Dans un système coopératif (qui passe par plusieurs noms, multi-tâches coopératives , multi-tâches nonpreemptive , threads de niveau utilisateur , fils verts , et les fibres sont cinq plus courantes actuellement) le programmeur est garanti que leur code fonctionnera atomiquement aussi longtemps que ils ne font aucun appel système ou appel yield(). Cela facilite particulièrement la gestion des structures de données partagées entre plusieurs fibres. Sauf si vous devez effectuer un appel système dans le cadre d'une section critique, les sections critiques n'ont pas besoin d'être marquées (avec mutex locket unlockappels, par exemple). Donc en code comme:

x = x + y
y = 2 * x

le programmeur n'a pas à s'inquiéter qu'une autre fibre puisse fonctionner avec les variables xet yen même temps. xet ysera mis à jour ensemble atomiquement du point de vue de toutes les autres fibres. De même, toutes les fibres pourraient partager une structure plus compliquée, comme un arbre et un appel similaire tree.insert(key, value)n'aurait pas besoin d'être protégé par un mutex ou une section critique.

En revanche, dans un système multithread préemptif, comme pour les threads réellement parallèles / multicœurs, chaque entrelacement d'instructions possible entre les threads est possible sauf s'il existe des sections critiques explicites. Une interruption et une préemption pourraient se produire entre deux instructions quelconques. Dans l'exemple ci-dessus:

 thread 0                thread 1
                         < thread 1 could read or modify x or y at this point
 read x
                         < thread 1 could read or modify x or y at this point
 read y
                         < thread 1 could read or modify x or y at this point
 add x and y
                         < thread 1 could read or modify x or y at this point
 write the result back into x
                         < thread 1 could read or modify x or y at this point
 read x
                         < thread 1 could read or modify x or y at this point
 multiply by 2
                         < thread 1 could read or modify x or y at this point
 write the result back into y
                         < thread 1 could read or modify x or y at this point

Donc, pour être correct sur un système préemptif ou sur un système avec des threads vraiment parallèles, vous devez entourer chaque section critique d'une sorte de synchronisation, comme un mutex lockau début et un mutex unlockà la fin.

Les fibres sont donc plus similaires aux bibliothèques d' E / S asynchrones qu'elles ne le sont aux threads préemptifs ou aux threads réellement parallèles. Le planificateur de fibre est appelé et peut changer de fibre pendant les opérations d'E / S à longue latence. Cela peut offrir plusieurs opérations d'E / S simultanées sans nécessiter d'opérations de synchronisation autour des sections critiques. Ainsi, l'utilisation de fibres peut, peut-être, avoir moins de complexité de programmation que les threads préemptifs ou vraiment parallèles, mais le manque de synchronisation autour des sections critiques conduirait à des résultats désastreux si vous tentiez d'exécuter les fibres vraiment simultanément ou de manière préventive.

Logique errante
la source
Je pense qu'il faudrait probablement mentionner 1. les systèmes hybrides où le système de threads au niveau utilisateur prend en charge la distribution de (nombreux) threads au niveau utilisateur sur (peu) cœurs de CPU et 2. le fait que lors de la programmation sur du "bare metal" , il est possible d'obtenir un traitement multiple sans préemption.
dfeuer
1
@dfeuer Je ne pense pas que la question demande toutes les différentes façons possibles de tirer parti du multitraitement. La question que je lis est "pourquoi les fibres (également appelées tâches non préemptives) ne peuvent-elles pas être traitées comme des fils préemptifs?" Si vous supposez un véritable parallélisme, vous devez alors vous synchroniser correctement, vous n'avez donc plus de "fibres".
Wandering Logic
1
Belle réponse. Les fibres ne peuvent pas garantir la sécurité car le programme supposerait qu'il a un accès exclusif aux ressources partagées jusqu'à ce qu'il spécifie un point d'interruption, où les threads supposent qu'un accès / mutation peut être effectué à tout moment; évidemment l'hypothèse la plus sûre lorsque plusieurs nœuds véritablement parallèles interagissent avec les mêmes données.
James M. Lay
6

La réponse est en fait qu'ils le pourraient, mais il y a un désir de ne pas le faire.

Les fibres sont utilisées car elles vous permettent de contrôler le déroulement de la planification. Par conséquent, il est beaucoup plus simple de concevoir certains algorithmes à l'aide de fibres car le programmeur a dit dans quelle fibre est exécutée à un moment donné. Cependant, si vous souhaitez que deux fibres soient exécutées sur deux cœurs différents en même temps, vous devez les planifier manuellement pour le faire.

Les threads permettent de contrôler le code exécuté sur le système d'exploitation. En échange, le système d'exploitation prend en charge de nombreuses tâches laides pour vous. Certains algorithmes deviennent plus difficiles, car le programmeur a moins son mot à dire dans quel code est exécuté à un moment donné, donc des cas plus inattendus peuvent apparaître. Des outils comme les mutex et les sémaphores sont ajoutés à un système d'exploitation pour donner au programmeur juste assez de contrôle pour rendre les threads utiles et réduire une partie de l'incertitude, sans embourber le programmeur.

Cela conduit à quelque chose qui est encore plus important que coopératif vs préemptif: les fibres sont contrôlées par le programmeur, tandis que les threads sont contrôlés par le système d'exploitation.

Vous ne voulez pas avoir à générer une fibre sur un autre processeur. Les commandes au niveau de l'assemblage pour ce faire sont atrocement compliquées, et elles sont souvent spécifiques au processeur. Vous ne voulez pas avoir à écrire 15 versions différentes de votre code pour gérer ces processeurs, alors vous vous tournez vers le système d'exploitation. Le travail de l'OS consiste à résumer ces différences. Le résultat est «threads».

Les fibres courent sur les fils. Ils ne courent pas seuls. Par conséquent, si vous souhaitez exécuter deux fibres sur des cœurs différents, vous pouvez simplement générer deux threads et exécuter une fibre sur chacun d'eux. Dans de nombreuses implémentations de fibres, vous pouvez le faire facilement. Le support multicœur ne provient pas des fibres, mais des fils.

Il devient facile de montrer que, sauf si vous voulez écrire votre propre code spécifique au processeur, vous ne pouvez rien faire en affectant des fibres à plusieurs cœurs que vous ne pouvez pas faire en créant des threads et en attribuant des fibres à chacun. L'une de mes règles préférées pour la conception d'API est "Une API n'est pas effectuée lorsque vous avez fini de tout y ajouter, mais plutôt lorsque vous ne trouvez plus rien à retirer." Étant donné que le multicœur est parfaitement géré en hébergeant des fibres sur des threads, il n'y a aucune raison de compliquer l'API fibre en ajoutant du multicœur à ce niveau.

Cort Ammon
la source