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 lock
et unlock
appels, 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 x
et y
en même temps. x
et y
sera 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 lock
au 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.
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.
la source