Übersicht über Synchronisierungsprimitiven
Aktualisiert: November 2007
In .NET Framework steht eine Reihe von Synchronisierungsprimitiven zur Verfügung, mit denen Interaktionen von Threads gesteuert und Racebedingungen verhindert werden können. Diese können grob in die folgenden drei Kategorien eingeteilt werden: Sperr-, Signalisierungs- und Interlocked-Vorgänge.
Diese Kategorien sind nicht präzise definiert: Einige Synchronisierungsmechanismen weisen Merkmale mehrerer Kategorien auf. So funktionieren Ereignisse, die jeweils einen einzelnen Thread freigeben, als Sperren, die Freigabe einer Sperre ähnelt jedoch einem Signal, und mithilfe von Interlocked-Vorgängen können Sperren konstruiert werden. Die Kategorien sind dennoch sinnvoll.
Sie sollten sich merken, dass die Threadsynchronisierung kooperativ erfolgt. Wenn auch nur ein Thread einen Synchronisierungsmechanismus umgeht und direkt auf die geschützte Ressource zugreift, kann der Synchronisierungsmechanismus seinen Zweck nicht erfüllen.
Sperrvorgänge
Eine Sperre verleiht einem einzelnen Thread oder einer festgelegten Anzahl von Threads die Steuerung einer Ressource. Ein Thread, der eine exklusive Sperre anfordert, wenn die Sperre gerade verwendet wird, blockiert so lange, bis die Sperre wieder verfügbar ist.
Exklusive Sperren
Das einfachste Sperrverfahren ist die lock-Anweisung in C# (SyncLock in Visual Basic), die den Zugriff auf einen Codeblock steuert. Ein solcher Block wird häufig als kritischer Abschnitt bezeichnet. Die lock-Anweisung wird mithilfe der Enter-Methode und der Exit-Methode der Monitor-Klasse implementiert. Sie stellt mit try…catch…finally sicher, dass die Sperre freigegeben wird.
Die Monitor-Klasse wird am besten verwendet, indem die lock-Anweisung zum Schützen kleiner Codeblöcke eingesetzt wird, die sich höchstens über eine einzelne Methode erstrecken. Die Monitor-Klasse ist zwar leistungsstark, jedoch auch anfällig für verwaiste Sperren und Deadlocks.
Monitor-Klasse
Die Monitor-Klasse stellt zusätzliche Funktionen bereit, die in Verbindung mit der lock-Anweisung verwendet werden können:
Die TryEnter-Methode ermöglicht es einem Thread, der durch das Warten auf eine Ressource blockiert ist, den Wartevorgang nach einer festgelegten Zeitspanne abzubrechen. Sie gibt einen booleschen Wert zurück, der Erfolg oder Fehlschlagen bedeutet, anhand dessen potenzielle Deadlocks erkannt und vermieden werden können.
Die Wait-Methode wird von einem Thread in einem kritischen Abschnitt aufgerufen. Dieser gibt die Steuerung der Ressource auf und blockiert, bis die Ressource wieder verfügbar ist.
Mithilfe der Pulse-Methode und der PulseAll-Methode kann ein Thread, der gerade eine Sperre freigeben oder Wait aufrufen will, einen oder mehrere Threads in die Bereitstellungswarteschlange einfügen, sodass diese die Sperre erhalten können.
Mithilfe von Timeouts für Wait-Methodenüberladungen können wartende Threads in die Bereitstellungswarteschlange überwechseln.
Wenn das für die Sperre verwendete Objekt von MarshalByRefObject abgeleitet ist, kann die Monitor-Klasse Sperrvorgänge in mehreren Anwendungsdomänen bereitstellen.
Monitor weist Threadaffinität auf. Das heißt, ein Thread, der in einem Monitor überwacht wird, muss durch Aufrufen von Exit oder Wait beendet werden.
Die Monitor-Klasse kann nicht instanziiert werden. Ihre Methoden sind statisch (Shared in Visual Basic) und werden für ein Sperrobjekt ausgeführt, das instanziiert werden kann.
Eine grundlegende Übersicht finden Sie unter Monitore.
Mutex-Klasse
Threads fordern einen Mutex an, indem sie eine Überladung seiner WaitOne-Methode aufrufen. Es werden Überladungen mit Timeouts bereitgestellt, mit denen Threads den Wartevorgang abbrechen können. Im Gegensatz zur Monitor-Klasse kann ein Mutex entweder lokal oder global sein. Globale Mutexe, auch als benannte Mutexe bezeichnet, sind im gesamten Betriebssystem sichtbar. Mit ihrer Hilfe können Threads in mehreren Anwendungsdomänen oder Prozessen synchronisiert werden. Lokale Mutexe sind von MarshalByRefObject abgeleitet und können über Anwendungsdomänengrenzen hinweg verwendet werden.
Darüber hinaus ist Mutex von WaitHandle abgeleitet, d. h., es kann mit den von WaitHandle bereitgestellten Signalisierungsmechanismen verwendet werden, z. B. mit den Methoden WaitAll, WaitAny und SignalAndWait.
Mutex weist wie Monitor Threadaffinität auf. Im Gegensatz zu Monitor ist ein Mutex ein Objekt, das nicht instanziiert werden kann.
Eine grundlegende Übersicht finden Sie unter Mutexe.
Sonstige Sperren
Sperren müssen nicht exklusiv sein. Es ist oft günstig, einer beschränkten Anzahl von Threads den gleichzeitigen Zugriff auf eine Ressource zu erlauben. Semaphore und Lese-/Schreibsperren sind für diese Art von zusammengeführtem Ressourcenzugriff ausgelegt.
ReaderWriterLock-Klasse
Die ReaderWriterLockSlim-Klasse ist für Situationen vorgesehen, in denen ein Thread, der Daten ändert (der Writer), einen exklusiven Zugriff auf die Ressource benötigt. Wenn der Writer nicht aktiv ist, kann eine beliebige Anzahl von Readern (z. B. durch Aufrufen der EnterReadLock-Methode) auf die Ressource zugreifen. Wenn ein Thread exklusiven Zugriff anfordert, (z. B. durch Aufrufen der EnterWriteLock-Methode), werden nachfolgende Readeranforderungen blockiert, bis alle vorhandenen Reader die Sperre beendet haben und der Writer die Sperre aktiviert und wieder beendet hat.
ReaderWriterLockSlim weist Threadaffinität auf.
Eine grundlegende Übersicht finden Sie unter Lese-/Schreibsperren.
Semaphore-Klasse
Die Semaphore-Klasse ermöglicht einer festgelegten Anzahl von Threads den Zugriff auf eine Ressource. Zusätzliche Threads, die die Ressource anfordern, werden blockiert, bis ein Thread das Semaphor freigibt.
Semaphore ist wie die Mutex-Klasse von WaitHandle abgeleitet. Semaphore kann genau wie Mutex lokal oder global sein. Es kann über Anwendungsdomänengrenzen hinweg verwendet werden.
Im Gegensatz zu Monitor, Mutex und ReaderWriterLock weist Semaphore keine Threadaffinität auf. Es kann daher in Szenarien verwendet werden, in denen ein Thread das Semaphor erhält und ein anderer es freigibt.
Eine grundlegende Übersicht finden Sie unter Semaphoren.
Signalisieren
Die einfachste Art, auf ein Signal von einem anderen Thread zu warten, ist das Aufrufen der Join-Methode, die so lange blockiert, bis der andere Thread beendet ist. Join verfügt über zwei Überladungen, über die ein Thread den Wartevorgang nach dem Ablauf einer festgelegten Zeitspanne abbrechen kann.
Wait-Handles stellen erheblich umfangreichere Warte- und Signalfunktionen bereit.
Wait-Handles
Wait-Handles sind von der WaitHandle-Klasse abgeleitet, die wiederum von MarshalByRefObject abgeleitet ist. Mit Wait-Handles können daher Aktivitäten von Threads über Anwendungsdomänengrenzen hinweg synchronisiert werden.
Threads blockieren Wait-Handles durch Aufrufen der WaitOne-Instanzmethode oder einer der statischen Methoden WaitAll, WaitAny oder SignalAndWait. Die Art der Freigabe hängt von der aufgerufenen Methode und der Art des Wait-Handles ab.
Eine grundlegende Übersicht finden Sie unter Wait-Handles.
Wait-Handles für Ereignisse
Zu Wait-Handles für Ereignisse gehören die EventWaitHandle-Klasse und ihre abgeleiteten Klassen AutoResetEvent und ManualResetEvent. Threads werden von einem Wait-Handle für ein Ereignis freigegeben, wenn dieses signalisiert wird, indem seine Set-Methode aufgerufen oder die SignalAndWait-Methode verwendet wird.
Wait-Handles für ein Ereignis setzen sich entweder automatisch selbst zurück, vergleichbar mit einem Drehkreuz, das bei jedem Signal nur jeweils einen Thread passieren lässt, oder sie müssen manuell zurückgesetzt werden, vergleichbar mit einer geschlossenen Tür, die bei einem Signal geöffnet wird und dann offen bleibt, bis jemand sie wieder schließt. Wie der Name schon erkennen lässt, stellen AutoResetEvent und ManualResetEvent die erste respektive zweite Möglichkeit dar.
Ein EventWaitHandle kann beide Ereignistypen darstellen und entweder lokal oder global sein. Die abgeleiteten Klassen AutoResetEvent und ManualResetEvent sind immer lokal.
Wait-Handles für ein Ereignis weisen keine Threadaffinität auf. Sie können von jedem Thread signalisiert werden.
Eine grundlegende Übersicht finden Sie unter EventWaitHandle, AutoResetEvent und ManualResetEvent.
Mutex-Klasse und Semaphore-Klasse
Da die Mutex-Klasse und die Semaphore-Klasse von WaitHandle abgeleitet sind, können sie mit den statischen Methoden von WaitHandle verwendet werden. Ein Thread kann z. B. mithilfe der WaitAll-Methode warten, bis alle drei folgenden Bedingungen erfüllt sind: Ein EventWaitHandle ist signalisiert und ein Mutex sowie ein Semaphore sind freigegeben. Entsprechend kann ein Thread mit der WaitAny-Methode warten, bis eine dieser Bedingungen erfüllt ist.
Für ein Mutex oder Semaphore bedeutet die Signalisierung, dass es freigegeben wird. Wenn einer der Typen als erstes Argument der SignalAndWait-Methode verwendet wird, wird er freigegeben. Bei einem Mutex, der Threadaffinität aufweist, wird eine Ausnahme ausgelöst, wenn der aufrufende Thread das Mutex nicht besitzt. Wie bereits erwähnt, weisen Semaphore keine Threadaffinität auf.
Interlocked-Vorgänge
Interlocked-Vorgänge sind einfache, unteilbare (atomische) Vorgänge, die von statischen Methoden der Interlocked-Klasse für einen Speicherort ausgeführt werden. Dazu gehören z. B. Addition, Inkrementieren und Dekrementieren, Austausch, bedingter Austausch, der auf einem Vergleich beruht, und Lesevorgänge für 64-Bit-Werte auf 32-Bit-Plattformen.
Hinweis: |
---|
Die Garantie der Unteilbarkeit gilt nur für individuelle Vorgänge. Für die Ausführung mehrerer Vorgänge in einer Einheit muss ein gröberer Synchronisierungsmechanismus verwendet werden. |
Diese Vorgänge sind zwar weder Sperren noch Signale, sie können jedoch zum Konstruieren von Sperren und Signalen verwendet werden. Da Interlocked-Vorgänge zum Betriebssystem Windows gehören, sind sie äußerst schnell.
Interlocked-Vorgänge können mit Garantien für flüchtigen Speicher verwendet werden, um Anwendungen zu schreiben, die leistungsstarke Parallelität ohne Sperren ermöglichen, jedoch eine ausgefeilte, systemnahe Programmierung erfordern. Einfache Sperren sind daher in den meisten Situationen die beste Wahl.
Eine grundlegende Übersicht finden Sie unter Interlocked-Vorgänge.
Siehe auch
Konzepte
Datensynchronisierung für Multithreading