Wie Sie in Ihrer Frage hervorheben, besteht der Hauptunterschied darin, ob der Scheduler jemals einen Thread vorwegnimmt oder nicht. Die Art und Weise, wie ein Programmierer über das Teilen von Datenstrukturen oder das Synchronisieren zwischen "Threads" nachdenkt, ist in präventiven und kooperativen Systemen sehr unterschiedlich.
In einem kooperativen System (das unter vielen Namen bekannt ist: kooperatives Multitasking , nicht präventives Multitasking , Threads auf Benutzerebene , grüne Threads und Fasern sind derzeit fünf gebräuchliche) wird dem Programmierer garantiert, dass sein Code atomar ausgeführt wird , solange Sie tätigen keine Systemaufrufe oder Anrufe yield()
. Dies macht es besonders einfach, mit Datenstrukturen umzugehen, die von mehreren Fasern gemeinsam genutzt werden. Sofern Sie keinen Systemaufruf als Teil eines kritischen Abschnitts ausführen müssen, müssen kritische Abschnitte nicht markiert werden ( z. B. mit Mutex lock
und unlock
Aufrufen). Also im Code wie:
x = x + y
y = 2 * x
Der Programmierer muss sich keine Sorgen machen, dass eine andere Faser gleichzeitig mit den Variablen x
und arbeiten könnte y
. x
und y
werden aus der Perspektive aller anderen Fasern atomar zusammen aktualisiert. In ähnlicher Weise könnten alle Fasern eine kompliziertere Struktur aufweisen, wie z. B. ein Baum, und ein Anruf wie tree.insert(key, value)
müsste nicht durch einen Mutex oder einen kritischen Abschnitt geschützt werden.
Im Gegensatz dazu ist in einem präemptiven Multithreading-System wie bei wirklich parallelen / Multicore-Threads jede mögliche Verschachtelung von Anweisungen zwischen Threads möglich, es sei denn, es gibt explizite kritische Abschnitte. Ein Interrupt und eine Preemption können zwischen zwei beliebigen Anweisungen auftreten. Im obigen Beispiel:
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
Um auf einem präventiven System oder auf einem System mit wirklich parallelen Threads korrekt zu sein, müssen Sie jeden kritischen Abschnitt mit einer Art Synchronisation umgeben, z. B. einem Mutex lock
am Anfang und einem Mutex unlock
am Ende.
Glasfasern sind daher asynchronen E / A- Bibliotheken ähnlicher als präventiven Threads oder wirklich parallelen Threads. Der Glasfaserplaner wird aufgerufen und kann Fasern während E / A-Operationen mit langer Latenz wechseln. Dies kann den Vorteil mehrerer gleichzeitiger E / A-Vorgänge bieten, ohne dass Synchronisierungsvorgänge in kritischen Abschnitten erforderlich sind. Daher kann die Verwendung von Fasern möglicherweise eine geringere Programmierkomplexität aufweisen als präemptive oder wirklich parallele Threads. Die fehlende Synchronisation um kritische Abschnitte würde jedoch zu katastrophalen Ergebnissen führen, wenn Sie versuchen würden, die Fasern wirklich gleichzeitig oder präemptiv auszuführen.
Die Antwort ist eigentlich, dass sie könnten, aber es besteht der Wunsch, dies nicht zu tun.
Fasern werden verwendet, weil Sie damit steuern können, wie die Planung erfolgt. Dementsprechend ist es viel einfacher, einige Algorithmen unter Verwendung von Fasern zu entwerfen, da der Programmierer zu sagen hat, in welcher Faser zu einem bestimmten Zeitpunkt ausgeführt wird. Wenn Sie jedoch möchten, dass zwei Fasern gleichzeitig auf zwei verschiedenen Kernen ausgeführt werden, müssen Sie sie manuell planen.
Threads geben die Kontrolle darüber, welcher Code auf dem Betriebssystem ausgeführt wird. Im Gegenzug erledigt das Betriebssystem viele hässliche Aufgaben für Sie. Einige Algorithmen werden schwieriger, da der Programmierer weniger Einfluss darauf hat, welcher Code zu einem bestimmten Zeitpunkt ausgeführt wird, sodass unerwartetere Fälle auftreten können. Tools wie Mutexe und Semaphoren werden einem Betriebssystem hinzugefügt, um dem Programmierer gerade genug Kontrolle zu geben, um Threads nützlich zu machen und einen Teil der Unsicherheit zu beseitigen, ohne den Programmierer zu stören.
Dies führt zu etwas, das noch wichtiger ist als kooperativ oder präventiv: Fasern werden vom Programmierer gesteuert, während Threads vom Betriebssystem gesteuert werden.
Sie möchten keine Faser auf einem anderen Prozessor erzeugen müssen. Die Befehle auf Baugruppenebene sind dafür äußerst kompliziert und häufig prozessorspezifisch. Sie möchten nicht 15 verschiedene Versionen Ihres Codes schreiben müssen, um diese Prozessoren zu handhaben, also wenden Sie sich an das Betriebssystem. Die Aufgabe des Betriebssystems ist es, diese Unterschiede zu abstrahieren. Das Ergebnis ist "Threads".
Fasern laufen über Fäden. Sie rennen nicht alleine. Wenn Sie also zwei Fasern auf unterschiedlichen Kernen ausführen möchten, können Sie einfach zwei Threads erzeugen und auf jedem von ihnen eine Faser ausführen. In vielen Implementierungen von Fasern können Sie dies leicht tun. Der Mehrkernträger kommt nicht von den Fasern, sondern von den Gewinden.
Es wird leicht zu zeigen, dass Sie, wenn Sie keinen eigenen prozessorspezifischen Code schreiben möchten, nichts tun können, indem Sie mehreren Kernen Fasern zuweisen, was Sie nicht tun könnten, indem Sie Threads erstellen und jedem Fasern zuweisen. Eine meiner Lieblingsregeln für das API-Design lautet: "Eine API wird nicht erstellt, wenn Sie alles hinzugefügt haben, sondern wenn Sie nichts mehr zum Herausnehmen finden." Angesichts der Tatsache, dass Multi-Core perfekt verarbeitet wird, indem Fasern auf Threads gehostet werden, gibt es keinen Grund, die Faser-API durch Hinzufügen von Multi-Core auf dieser Ebene zu komplizieren.
quelle