Best Practices für verwaltetes Threading

Wenn Sie mehrere Threads verwenden, ist eine sorgfältige Programmierung erforderlich. Für die meisten Aufgaben können Sie die Komplexität reduzieren, indem Sie Ausführungsanforderungen mithilfe von Threadpoolthreads in Warteschlangen einfügen. Dieses Thema behandelt problematische Situationen wie die Koordinierung der Verarbeitung von mehreren Threads oder die Behandlung von blockierenden Threads.

Hinweis

Ab .NET Framework 4 stellen die Task Parallel Library und PLINQ APIs bereit, die die Komplexität und Risiken der Multithreadprogrammierung etwas reduzieren. Weitere Informationen finden Sie unter Parallele Programmierung in .NET.

Deadlocks und Racebedingungen

Das Multithreading löst Probleme mit dem Durchsatz und der Ansprechempfindlichkeit, verursacht dabei jedoch neue Probleme: Deadlocks und Racebedingungen.

Deadlocks

Ein Deadlock liegt vor, wenn zwei Threads gleichzeitig versuchen, eine Ressource zu sperren, die der jeweils andere Thread bereits gesperrt hat. Keiner der beiden Threads kann weiter fortgesetzt werden.

Viele Methoden der Klassen des verwalteten Threadings stellen Timeouts bereit, mit deren Hilfe Sie Deadlocks entdecken können. Im folgenden Code wird beispielsweise versucht, ein Objekt namens lockObject zu sperren. Wenn die Sperrung nicht innerhalb von 300 Millisekunden erfolgt, gibt Monitor.TryEnter den Wert false zurück.

If Monitor.TryEnter(lockObject, 300) Then  
    Try  
        ' Place code protected by the Monitor here.  
    Finally  
        Monitor.Exit(lockObject)  
    End Try  
Else  
    ' Code to execute if the attempt times out.  
End If  
if (Monitor.TryEnter(lockObject, 300)) {  
    try {  
        // Place code protected by the Monitor here.  
    }  
    finally {  
        Monitor.Exit(lockObject);  
    }  
}  
else {  
    // Code to execute if the attempt times out.  
}  

Racebedingungen

Bei einer Racebedingung handelt es sich um einen Fehler, der auftritt, wenn das Ergebnis eines Programms davon abhängt, welcher von zwei oder mehr Threads einen bestimmten Codeblock zuerst erreicht. Wenn Sie das Programm mehrmals ausführen, werden verschiedene Ergebnisse erzeugt, und das Ergebnis einer bestimmten Ausführung lässt sich nicht vorhersagen.

Ein einfaches Beispiel einer Racebedingung ist die Erhöhung eines Feldes. Angenommen, eine Klasse besitzt ein privates static-Feld (Shared in Visual Basic), das jedes Mal, wenn eine Instanz der Klasse erstellt wird, mit Code wie objCt++; (C#) oder objCt += 1 (Visual Basic) erhöht wird. Für diesen Vorgang muss der Wert von objCt in ein Register geladen, der Wert erhöht und in objCt gespeichert werden.

In einer Multithreadanwendung kann ein Thread, der den Wert geladen und erhöht hat, von einem anderen Thread präemptiv unterbrochen werden, der dann alle drei Schritte ausführt. Wenn der erste Thread die Ausführung fortsetzt und den Wert speichert, überschreibt er objCt, ohne dass dabei berücksichtigt wird, dass der Wert in der Zwischenzeit geändert wurde.

Diese spezielle Racebedingung lässt sich mit den Methoden der Interlocked-Klasse (z. B. Interlocked.Increment) problemlos vermeiden. Weitere Techniken zum Synchronisieren von Daten zwischen mehreren Threads finden Sie unter Synchronizing Data for Multithreading (Synchronisieren von Daten für Multithreading).

Racebedingungen können auch auftreten, wenn Sie die Aktivitäten von mehreren Threads synchronisieren. Bei jeder Codezeile, die Sie schreiben, müssen Sie sich überlegen, was passieren kann, wenn ein Thread vor der Ausführung der Zeile (oder jeder einzelnen Anweisung, aus denen die Zeile besteht) präemptiv unterbrochen und die Ausführung von einem anderen Thread fortgesetzt wird.

Statische Member und statische Konstruktoren

Eine Klasse wird erst dann initialisiert, wenn ihr Klassenkonstruktor (static-Konstruktor in C#, Shared Sub New in Visual Basic) nicht mehr ausgeführt wird. Um die Codeausführung für einen nicht initialisierten Typ zu verhindern, blockiert die Common Language Runtime alle Aufrufe von anderen Threads an static-Member der Klasse (Shared-Member in Visual Basic), bis der Klassenkonstruktor nicht mehr ausgeführt wird.

Wenn ein Klassenkonstruktor zum Beispiel einen neuen Thread startet und die Threadprozedur einen static-Member der Klasse aufruft, wird der neue Thread blockiert, bis der Klassenkonstruktor beendet wird.

Dies gilt für jeden Typ, der über einen static-Konstruktor verfügen kann.

Anzahl von Prozessoren

Die Multithreadarchitektur kann davon beeinflusst werden, ob das System mehrere Prozessoren oder nur einen Prozessor enthält. Weitere Informationen finden Sie unter Anzahl von Prozessoren.

Verwenden Sie die Eigenschaft Environment.ProcessorCount, um die zur Laufzeit verfügbare Anzahl von Prozessoren zu bestimmen.

Allgemeine Empfehlungen

Beachten Sie bei Verwendung von mehreren Threads die folgenden Richtlinien:

  • Verwenden Sie Thread.Abort nicht, um andere Threads zu beenden. Wenn Sie Abort für einen anderen Thread aufrufen, wird für diesen Thread mit hoher Wahrscheinlichkeit eine Ausnahme ausgelöst, ohne dass Sie wissen, an welchem Punkt die Verarbeitung unterbrochen wurde.

  • Verwenden Sie Thread.Suspend und Thread.Resume, nicht, um die Aktivitäten mehrerer Threads zu synchronisieren. Verwenden Sie Mutex, ManualResetEvent, AutoResetEvent und Monitor.

  • Steuern Sie die Ausführung von Arbeitsthreads nicht vom Hauptprogramm aus (beispielsweise unter Verwendung von Ereignissen). Konzipieren Sie das Programm hingegen so, dass Arbeitsthreads dafür zuständig sind, auf auszuführende Aufgaben zu warten, diese auszuführen und die anderen Teile des Programms darüber zu informieren, dass die Aufgaben erledigt wurden. Bei nicht blockierenden Arbeitsthreads sollten Sie u. U. Threadpoolthreads verwenden. Monitor.PulseAll ist in Situationen nützlich, in denen Arbeitsthreads blockieren.

  • Verwenden Sie keine Typen als Sperrobjekte. Das bedeutet, dass Sie Code wie lock(typeof(X)) in C# oder SyncLock(GetType(X)) in Visual Basic genauso wie die Verwendung von Monitor.Enter mit Type-Objekten vermeiden sollten. Für einen entsprechenden Typ gibt es nur eine Instanz von System.Type pro Anwendungsdomäne. Wenn der von Ihnen gesperrte Typ öffentlich ist, kann er von fremdem Code gesperrt werden, was zu Deadlocks führt. Weitere Informationen finden Sie unter Reliability Best Practices (Empfohlene Vorgehensweise für Zuverlässigkeit).

  • Seien Sie beim Sperren von Instanzen vorsichtig, zum Beispiel lock(this) in C# oder SyncLock(Me) in Visual Basic. Wenn anderer, für den Typ externer Code in der Anwendung das Objekt sperrt, könnte dies zu Deadlocks führen.

  • Stellen Sie sicher, dass ein Thread, der in einem Monitor überwacht wird, den Monitor verlässt, auch wenn eine Ausnahme auftritt, während der Thread sich im Monitor befindet. Die C#-Anweisung lock und die Visual Basic-Anweisung SyncLock stellen dieses Verhalten automatisch bereit, wobei sie einen finally-Block verwenden, um sicherzustellen, dass Monitor.Exit aufgerufen wird. Wenn Sie den Aufruf von Exit nicht sicherstellen können, empfiehlt sich stattdessen die Verwendung von Mutex. Ein Mutex wird automatisch freigegeben, wenn der Thread, in dessen Besitz er sich befindet, beendet wird.

  • Verwenden Sie mehrere Threads nicht für Aufgaben, für die verschiedene Ressourcen benötigt werden, und vermeiden Sie es, mehrere Threads einer einzelnen Ressource zuzuweisen. Aufgaben, die Ein- und Ausgaben umfassen, sollten beispielsweise über einen eigenen Thread verfügen, da der entsprechende Thread während der E/A-Vorgänge blockiert wird. Dies ermöglicht die Ausführung von anderen Threads. Benutzereingaben sind ein weiteres Beispiel für eine Ressource, die von einem für sie reservierten Thread profitiert. Auf einem Computer mit einem einzelnen Prozessor können eine Aufgabe, die eine ressourcenintensive Berechnung erfordert, und Benutzereingaben oder Aufgaben mit E/A-Vorgängen problemlos gleichzeitig ausgeführt werden. Mehrere rechenintensive Aufgaben machen sich jedoch gegenseitig die Ressourcen streitig.

  • Möglicherweise empfiehlt es sich, für einfache Zustandsänderungen statt der Interlocked-Anweisung (lock in Visual Basic) Methoden der SyncLock-Klasse zu verwenden. Die lock-Anweisung ist für allgemeine Zwecke gut geeignet, doch für Aktualisierungen, die atomar sein müssen, bietet die Interlocked-Klasse eine bessere Leistung. Sie führt intern ein einzelnes lock-Präfix aus, wenn kein Konflikt vorliegt. Achten Sie bei Codeüberprüfungen auf Code wie in den folgenden Beispielen. Im ersten Beispiel wird eine Zustandsvariable erhöht:

    SyncLock lockObject  
        myField += 1  
    End SyncLock  
    
    lock(lockObject)
    {  
        myField++;  
    }  
    

    Sie können die Leistung verbessern, indem Sie statt der Increment-Anweisung die lock-Methode wie folgt verwenden:

    System.Threading.Interlocked.Increment(myField)  
    
    System.Threading.Interlocked.Increment(myField);  
    

    Hinweis

    Verwenden Sie die Add-Methode für atomische Steigerungen größer 1.

    Im zweiten Beispiel wird eine Referenztypvariable nur aktualisiert, wenn es sich um einen NULL-Verweis (Nothing in Visual Basic) handelt.

    If x Is Nothing Then  
        SyncLock lockObject  
            If x Is Nothing Then  
                x = y  
            End If  
        End SyncLock  
    End If  
    
    if (x == null)  
    {  
        lock (lockObject)  
        {  
            x ??= y;
        }  
    }  
    

    Die Leistung kann verbessert werden, indem stattdessen die CompareExchange-Methode wie folgt verwendet wird:

    System.Threading.Interlocked.CompareExchange(x, y, Nothing)  
    
    System.Threading.Interlocked.CompareExchange(ref x, y, null);  
    

    Hinweis

    Die Überladung der CompareExchange<T>(T, T, T)-Methode stellt eine typsichere Alternative für Verweistypen dar.

Empfehlungen für Klassenbibliotheken

Beachten Sie die folgenden Richtlinien, wenn Sie Klassenbibliotheken für Multithreading entwerfen:

  • Vermeiden Sie nach Möglichkeit die Notwendigkeit einer Synchronisierung. Dies gilt besonders für häufig verwendeten Code. Beispielsweise kann ein Algorithmus angepasst werden, um eine Racebedingung zu tolerieren und nicht zu unterbinden. Durch unnötige Synchronisierung wird die Leistung verringert, und es entsteht die Gefahr von Deadlocks und Racebedingungen.

  • Machen Sie statische Daten (Shared in Visual Basic) standardmäßig threadsicher.

  • Legen Sie Instanzdaten nicht als standardmäßig threadsicher fest. Durch Hinzufügen von Sperren, um threadsicheren Code zu erstellen, wird die Leistung beeinträchtigt, die Sperrenkonflikte werden erhöht, und es entsteht die Gefahr von Deadlocks. Bei den gängigen Anwendungsmodellen führt immer nur ein Thread Benutzercode aus, wodurch die Notwendigkeit der Threadsicherheit reduziert wird. Aus diesem Grund sind .NET-Klassenbibliotheken per Standard nicht threadsicher.

  • Vermeiden Sie die Bereitstellung von statischen Methoden, die den statischen Zustand ändern. Bei den üblichen Serverszenarios wird der statische Zustand in mehreren Anforderungen gemeinsam genutzt, d. h., mehrere Threads können den Code gleichzeitig ausführen. Dadurch werden Threadingfehler möglich. Verwenden Sie u. U. ein Entwurfsmuster, bei dem Daten in Instanzen gekapselt werden, die nicht in mehreren Anforderungen gemeinsam genutzt werden. Darüber hinaus können bei der Synchronisierung statischer Daten Aufrufe zwischen statischen Methoden, die den Zustand ändern, zu Deadlocks oder redundanter Synchronisierung führen und die Leistung beeinträchtigen.

Siehe auch