Threadsynchronisierung (C#-Programmierhandbuch)

Aktualisiert: November 2007

In den folgenden Abschnitten werden die Features und Klassen beschrieben, mit denen der Zugriff auf Ressourcen in Multithreadanwendungen synchronisiert werden kann.

Einer der Vorteile von mehreren Threads in einer Anwendung besteht darin, dass jeder Thread asynchron ausgeführt wird. In Windows-Anwendungen können auf diese Weise zeitaufwändige Aufgaben im Hintergrund ausgeführt werden, während die Anwendungsfenster und Steuerelemente weiterhin reagieren. Für Serveranwendungen bietet Multithreading die Möglichkeit, jede eingehende Anforderung mit einem anderen Thread zu behandeln. Andernfalls würden neue Anforderungen erst verarbeitet, wenn die Verarbeitung der jeweils vorhergehenden Anforderung vollständig abgeschlossen ist.

Aufgrund des asynchronen Charakters von Threads muss allerdings der Zugriff auf Ressourcen wie Dateihandles, Netzwerkverbindungen und den Speicher koordiniert werden. Andernfalls greifen möglicherweise zwei (oder mehr) Threads gleichzeitig auf dieselbe Ressource zu, ohne von den Aktionen des jeweils anderen zu wissen. Dies führt zu nicht vorhersehbaren Beschädigungen von Daten.

Für einfache Operationen für ganzzahlige numerische Datentypen erfolgt die Threadsynchronisierung mit Membern der Interlocked-Klasse. Für alle anderen Datentypen und nicht threadsicheren Ressourcen kann Multithreading nur mit den in diesem Thema beschriebenen Konstrukten sicher ausgeführt werden.

Hintergrundinformationen zur Multithreadprogrammierung finden Sie unter:

Das lock-Schlüsselwort

Mithilfe des lock-Schlüsselworts kann sichergestellt werden, dass ein Codeblock ohne Unterbrechung durch andere Threads vollständig abgeschlossen wird. Dazu wird während der Ausführung des Codeblocks für ein bestimmtes Objekt eine Sperre für gegenseitigen Ausschluss eingerichtet.

Eine lock-Anweisung beginnt mit dem lock-Schlüsselwort, dem ein Objekt als Argument zugewiesen wird. Darauf folgt ein Codeblock, der nur von einem Thread gleichzeitig ausgeführt werden darf. Beispiel:

public class TestThreading
{
    private System.Object lockThis = new System.Object();

    public void Function()
    {

        lock (lockThis)
        {
            // Access thread-sensitive resources.
        }
    }

}

Bei dem dem lock-Schlüsselwort bereitgestellten Argument muss es sich um ein Objekt auf der Basis eines Referenztyps handeln. Es wird zur Festlegung des Umfangs der Sperre verwendet. Im obigen Beispiel ist die Sperre auf diese Funktion beschränkt, da außerhalb dieser Funktion keine Verweise auf das Objekt lockThis existieren. Wenn ein solcher Verweis vorhanden ist, wird die Sperre auf das Objekt ausgedehnt. Genau genommen wird das lock bereitgestellte Objekt nur dazu verwendet, die von mehreren Threads gemeinsam genutzte Ressource eindeutig zu bezeichnen. Es kann also eine beliebige Klasseninstanz sein. In der Praxis stellt dieses Objekt jedoch normalerweise die Ressource dar, für die die Threadsynchronisierung erforderlich ist. Wenn ein Containerobjekt beispielsweise von mehreren Threads verwendet werden soll, kann der Container für die Sperre übergeben werden, und der synchronisierte Codeblock hinter der Sperre greift auf den Container zu. Solange andere Threads denselben Container sperren, bevor sie darauf zugreifen, ist der Zugriff auf das Objekt sicher synchronisiert.

In der Regel sollten Sie Sperren von public-Typen oder Objektinstanzen, die nicht durch die Anwendung gesteuert werden, vermeiden. lock(this) kann z. B. problematisch sein, wenn auf die Instanz öffentlich zugegriffen werden kann, da das Objekt auch durch Code gesperrt werden kann, auf den Sie keinen Einfluss haben. Dies könnte Deadlocks verursachen, bei denen zwei oder mehr Threads auf die Freigabe desselben Objekts warten. Das Sperren von öffentlichen Datentypen (die sich von öffentlichen Objekten unterscheiden) kann aus demselben Grund Probleme verursachen. Vor allem das Sperren von Zeichenfolgenliteralen ist riskant, da Zeichenfolgenliterale von der Common Language Runtime (CLR) intern gespeichert werden. Das bedeutet, dass von jedem Zeichenfolgenliteral nur eine einzige Instanz für das gesamte Programm vorhanden ist. Dasselbe Objekt stellt das Literal in allen ausgeführten Anwendungsdomänen auf allen Threads dar. Demzufolge führt eine Sperre, die auf eine Zeichenfolge mit demselben Inhalt im gesamten Anwendungsprozess angewendet wird, zur Sperrung sämtlicher Instanzen dieser Zeichenfolge in der Anwendung. Daher ist es am günstigsten, private oder geschützte Member zu sperren, die nicht intern gespeichert werden. Einige Klassen stellen spezifische Member für Sperren bereit. Der Array-Typ stellt beispielsweise SyncRoot bereit. Viele Auflistungstypen stellen auch einen SyncRoot-Member bereit.

Weitere Informationen zum lock-Schlüsselwort finden Sie unter:

Monitore

Wie das lock-Schlüsselwort verhindern auch Monitore die gleichzeitige Ausführung von Codeblöcken durch mehrere Threads. Mit der Enter-Methode kann nur ein einziger Thread die Ausführung der folgenden Anweisungen fortsetzen. Alle anderen Threads werden blockiert, bis der ausführende Thread Exit aufruft. Dieser Vorgang entspricht der Verwendung des lock-Schlüsselworts. Und in der Tat wird das lock-Schlüsselwort mit der Monitor-Klasse implementiert. Beispiel:

lock (x)
{
    DoSomething();
}

Dies ist identisch mit:

System.Object obj = (System.Object)x;
System.Threading.Monitor.Enter(obj);
try
{
    DoSomething();
}
finally
{
    System.Threading.Monitor.Exit(obj);
}

Normalerweise ist die Verwendung des lock-Schlüsselworts der direkten Verwendung der Monitor-Klasse vorzuziehen. Zum einen ist lock übersichtlicher, zum anderen stellt lock sicher, dass der zugrunde liegende Monitor freigegeben wird, selbst wenn der geschützte Code eine Ausnahme auslöst. Dies wird mithilfe des finally-Schlüsselworts erreicht, das den zugehörigen Codeblock unabhängig davon ausführt, ob eine Ausnahme ausgelöst wurde.

Weitere Informationen zu Monitoren finden Sie unter Technologiebeispiel für Monitor-Synchronisierung.

Synchronisierungsereignisse und Wait-Handles

Mit Sperren oder Monitoren können Sie die gleichzeitige Ausführung von threadempfindlichen Codeblöcken verhindern, doch diese Konstrukte ermöglichen es Ihnen nicht, Ereignisse zwischen Threads zu kommunizieren. Dazu sind Synchronisierungsereignisse erforderlich. Dies sind Objekte, die einen von zwei Zuständen aufweisen (signalisiert oder nicht signalisiert), mit denen Threads aktiviert und unterbrochen werden können. Threads können unterbrochen werden, indem sie zum Warten auf ein nicht signalisiertes Synchronisierungsereignis veranlasst werden, und sie können aktiviert werden, indem der Zustand des Ereignisses auf signalisiert geändert wird. Wenn ein Thread versucht, auf ein Ereignis zu warten, das bereits signalisiert wurde, wird die Ausführung des Threads ohne Verzögerung fortgesetzt.

Es gibt zwei Arten von Synchronisierungsereignissen: AutoResetEvent und ManualResetEvent. Der einzige Unterschied zwischen den beiden besteht darin, dass AutoResetEvent automatisch von signalisiert zu nicht signalisiert geändert wird, wenn das Ereignis einen Thread aktiviert. Umgekehrt ist es mit ManualResetEvent möglich, eine beliebige Anzahl von Threads über den signalisierten Zustand zu aktivieren, und das Ereignis wird nur in den nicht signalisierten Zustand zurückgesetzt, wenn seine Reset-Methode aufgerufen wird.

Threads können zum Warten auf Ereignisse veranlasst werden, indem eine der Wait-Methoden (z. B. WaitOne, WaitAny oder WaitAll) aufgerufen wird. WaitHandle.WaitOne() sorgt dafür, dass der Thread wartet, bis ein einzelnes Ereignis signalisiert wird, WaitHandle.WaitAny() blockiert einen Thread, bis mindestens eines der angegebenen Ereignisse signalisiert wird, und WaitHandle.WaitAll() blockiert den Thread, bis alle angegebenen Ereignisse signalisiert werden. Ein Ereignis wird signalisiert, wenn seine Set-Methode aufgerufen wird.

Im folgenden Beispiel wird ein Thread erstellt und durch die Main-Funktion gestartet. Der neue Thread wartet mit der WaitOne-Methode auf ein Ereignis. Der Thread wird solange unterbrochen, bis das Ereignis durch den primären Thread, der die Main-Funktion ausführt, signalisiert wird. Sobald das Ereignis signalisiert wird, wird der Hilfsthread wieder aktiviert. Da in diesem Fall nur ein Thread mit dem Ereignis aktiviert wird, kann entweder die AutoResetEvent-Klasse oder ManualResetEvent-Klasse verwendet werden.

using System;
using System.Threading;

class ThreadingExample
{
    static AutoResetEvent autoEvent;

    static void DoWork()
    {
        Console.WriteLine("   worker thread started, now waiting on event...");
        autoEvent.WaitOne();
        Console.WriteLine("   worker thread reactivated, now exiting...");
    }

    static void Main()
    {
        autoEvent = new AutoResetEvent(false);

        Console.WriteLine("main thread starting worker thread...");
        Thread t = new Thread(DoWork);
        t.Start();

        Console.WriteLine("main thread sleeping for 1 second...");
        Thread.Sleep(1000);

        Console.WriteLine("main thread signaling worker thread...");
        autoEvent.Set();
    }
}

Weitere Beispiele zur Verwendung von Threadsynchronisierungsereignissen finden Sie unter:

Mutex-Objekt

Ein Mutex ähnelt einem Monitor. Er verhindert die gleichzeitige Ausführung eines Codeblocks durch mehr als einen Thread. Die Bezeichnung "Mutex" ist eine Abkürzung des englischen Begriffs für "sich gegenseitig ausschließend" (mutually exclusive). Im Unterschied zu Monitoren können Sie allerdings mit einem Mutex Threads auch über mehrere Prozesse synchronisieren. Ein Mutex wird durch die Mutex-Klasse dargestellt.

Wenn ein Mutex zur Synchronisierung über Prozesse hinweg verwendet wird, wird er als benannter Mutex bezeichnet, da er von einer anderen Anwendung verwendet werden soll und somit nicht mithilfe einer globalen oder statischen Variablen gemeinsam genutzt werden kann. Sie müssen ihm einen Namen zuweisen, damit beide Anwendungen auf das gleiche Mutexobjekt zugreifen können.

Es ist zwar möglich, einen Mutex zur Threadsynchronisierung innerhalb eines Prozesses zu verwenden, doch generell ist die Verwendung von Monitor vorzuziehen, da Monitore speziell für .NET Framework entwickelt wurden und daher die Ressourcen optimaler nutzen. Im Gegensatz dazu ist die Mutex-Klasse ein Wrapper für ein Win32-Konstrukt. Ein Mutex ist zwar leistungsstärker als ein Monitor, benötigt aber auch Interop-Übergänge, die deutlich rechenintensiver sind als die von der Monitor-Klasse benötigten. Ein Beispiel zur Verwendung von Mutexen finden Sie unter Mutexe.

Verwandte Abschnitte

Siehe auch

Konzepte

C#-Programmierhandbuch

Referenz

Thread

WaitOne

WaitAny

WaitAll

Monitor

Mutex

AutoResetEvent

ManualResetEvent

Interlocked

WaitHandle

Weitere Ressourcen

Implementieren des asynchronen CLR-Programmiermodells

Vereinfachter APM mit C#

Deadlockbildschirm