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.