Threadsynchronisierung (C# und Visual Basic)

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:

Die Sperr- und SyncLock-Schlüsselwörter

Mithilfe der Anweisungen lock (C#) und SyncLock (Visual Basic) wird sichergestellt, dass ein Codeblock bis zum Ende ausgeführt wird, ohne dass Unterbrechungen durch andere Threads stattfinden. Dazu wird während der Ausführung des Codeblocks für ein bestimmtes Objekt eine Sperre für gegenseitigen Ausschluss eingerichtet.

Einer lock oder SyncLock-Anweisung wird ein Objekt als Argument zugewiesen. Darauf folgt ein Codeblock, der nur von einem Thread gleichzeitig ausgeführt werden darf. Beispiele:

Public Class TestThreading
    Dim lockThis As New Object

    Public Sub Process()
        SyncLock lockThis
            ' Access thread-sensitive resources.
        End SyncLock
    End Sub
End Class
public class TestThreading
{
    private System.Object lockThis = new System.Object();

    public void Process()
    {

        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 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 über lock- und SyncLock-Ausdrücke finden Sie unter den folgenden Themen:

Monitore

Wie die lock- und SyncLock-Schlüsselwörter 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. Beispiele:

SyncLock x
    DoSomething()
End SyncLock
lock (x)
{
    DoSomething();
}

Dies ist identisch mit:

Dim obj As Object = CType(x, Object)
System.Threading.Monitor.Enter(obj)
Try
    DoSomething()
Finally
    System.Threading.Monitor.Exit(obj)
End Try
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 (C#) oder SyncLock (Visual Basic)-Schlüsselworts der direkten Verwendung der Monitor-Klasse vorzuziehen. Zum einen ist lock oder SyncLock übersichtlicher, zum anderen stellt lock oder SyncLock 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.

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.

Imports System.Threading

Module Module1
    Dim autoEvent As AutoResetEvent

    Sub DoWork()
        Console.WriteLine("   worker thread started, now waiting on event...")
        autoEvent.WaitOne()
        Console.WriteLine("   worker thread reactivated, now exiting...")
    End Sub

    Sub Main()
        autoEvent = New AutoResetEvent(False)

        Console.WriteLine("main thread starting worker thread...")
        Dim t As New Thread(AddressOf DoWork)
        t.Start()

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

        Console.WriteLine("main thread signaling worker thread...")
        autoEvent.Set()
    End Sub
End Module
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();
    }
}

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.

Interlocked-Klasse

Mit den Methoden der Interlocked-Klasse können Sie Probleme verhindern, die auftreten können, wenn mehrere Threads gleichzeitig versuchen, den gleichen Wert zu aktualisieren oder zu vergleichen. Mit den Methoden dieser Klasse können Sie Werte aus beliebigen Threads auf sichere Weise schrittweise erhöhen oder verringern, austauschen und vergleichen.

ReaderWriter-Sperren

In manchen Fällen soll eine Ressource vielleicht nur gesperrt werden, wenn Daten geschrieben werden, und mehrere Clients sollen gleichzeitig Daten lesen können, wenn Daten nicht aktualisiert werden. Die ReaderWriterLock-Klasse gewährleistet exklusiven Zugriff auf eine Ressource, während ein Thread die Ressource ändert, erlaubt jedoch einen nicht exklusiven Zugriff beim Lesen der Ressource. ReaderWriter-Sperren sind eine nützliche Alternative zu exklusiven Sperren, die andere Threads in den Wartezustand zwingen, selbst wenn diese gar keine Daten aktualisieren müssen.

Deadlocks

Auch wenn die Threadsynchronisierung in Multithreadanwendungen sehr nützlich ist, besteht immer die Gefahr eines deadlock, bei dem mehrere Threads aufeinander warten und die Anwendung dadurch in eine Endlosschleife gerät. Deadlocks können mit der Situation an einer Kreuzung verglichen werden, bei der vier Autos darauf warten, dass eines losfährt. Die Vermeidung von Deadlocks ist wichtig und erfordert sorgfältige Planung. Oft können Sie Deadlocks vorhersagen, indem Sie ein Diagramm einer Multithreadanwendung erstellen, bevor Sie mit der Codierung beginnen.

Verwandte Abschnitte

Gewusst wie: Verwenden eines Threadpools (C# und Visual Basic)

SO WIRD'S GEMACHT: Synchronisieren des Zugriffs auf eine freigegebene Ressource in einer Multithreading-Umgebung mit Visual C# .NET

Wie Sie einen Thread mit Visual c# erstellen (maschinell übersetzt)

Wie Sie mithilfe von Visual c# eine Arbeitsaufgabe an den Threadpool senden (maschinell übersetzt)

SO WIRD'S GEMACHT: Synchronisieren des Zugriffs auf eine freigegebene Ressource in einer Multithreading-Umgebung mit Visual C# .NET

Siehe auch

Referenz

SyncLock-Anweisung

lock-Anweisung (C#-Referenz)

Thread

WaitOne

WaitAny

WaitAll

Join

Start

Sleep

Monitor

Mutex

AutoResetEvent

ManualResetEvent

Interlocked

WaitHandle

EventWaitHandle

System.Threading

Set

Konzepte

Multithreadanwendungen (C# und Visual Basic)

Mutexe

Monitore

Interlocked-Vorgänge

AutoResetEvent

Datensynchronisierung für Multithreading

Weitere Ressourcen

Implementieren des asynchronen CLR-Programmiermodells

Vereinfachter APM mit C#

Deadlockbildschirm

Multithreading in Komponenten

Gewusst wie: Synchronisieren des Zugriffs auf eine freigegebene Ressource in einer Multithreadumgebung mithilfe von Visual C# .NET