Racebedingungen und Deadlocks

Visual Basic .NET oder Visual Basic bietet zum ersten Mal die Möglichkeit, Threads in Visual Basic-Anwendungen zu verwenden. Threads führen Debugprobleme wie Racebedingungen und Deadlocks ein. In diesem Artikel werden diese beiden Probleme behandelt.

Ursprüngliche Produktversion: Visual Basic, Visual Basic .NET
Ursprüngliche KB-Nummer: 317723

Wenn Racebedingungen auftreten

Eine Racebedingung tritt auf, wenn zwei Threads gleichzeitig auf eine freigegebene Variable zugreifen. Der erste Thread liest die Variable, und der zweite Thread liest denselben Wert aus der Variablen. Dann führen der erste und der zweite Thread ihre Vorgänge für den Wert aus, und sie rennen, um zu sehen, welcher Thread den Wert zuletzt in die freigegebene Variable schreiben kann. Der Wert des Threads, der seinen Wert zuletzt schreibt, wird beibehalten, da der Thread über den Wert schreibt, den der vorherige Thread geschrieben hat.

Details und Beispiele für eine Racebedingung

Jedem Thread wird ein vordefinierter Zeitraum für die Ausführung auf einem Prozessor zugewiesen. Wenn die für den Thread zugewiesene Zeit abläuft, wird der Kontext des Threads bis zum nächsten Einschalten des Prozessors gespeichert, und der Prozessor beginnt mit der Ausführung des nächsten Threads.

Wie kann ein einzeilige Befehle eine Racebedingung verursachen?

Sehen Sie sich das folgende Beispiel an, um zu sehen, wie eine Racebedingung auftritt. Es gibt zwei Threads, und beide aktualisieren eine freigegebene Variable namens total (die im Assemblycode als dword ptr ds:[031B49DCh] dargestellt wird).

  • Thread 1

    Total = Total + val1
    
  • Thread 2

    Total = Total - val2
    

Assemblycode (mit Zeilennummern) aus der Kompilierung des vorangehenden Visual Basic-Codes:

  • Thread 1

    1. mov eax,dword ptr ds:[031B49DCh]
    2. add eax,edi
    3. jno 00000033
    4. xor ecx,ecx
    5. call 7611097F
    6. mov dword ptr ds:[031B49DCh],eax
    
  • Thread 2

    1. mov eax,dword ptr ds:[031B49DCh]
    2. sub eax,edi
    3. jno 00000033
    4. xor ecx,ecx
    5. call 76110BE7
    6. mov dword ptr ds:[031B49DCh],eax
    

Wenn Sie sich den Assemblycode ansehen, können Sie sehen, wie viele Vorgänge der Prozessor auf der unteren Ebene ausführt, um eine einfache Additionsberechnung auszuführen. Ein Thread kann während seiner Zeit auf dem Prozessor möglicherweise den gesamten Assemblycode oder einen Teil davon ausführen. Sehen Sie sich nun an, wie eine Racebedingung aus diesem Code auftritt.

Total ist 100, val1 ist 50 und val2 ist 15. Thread 1 erhält die Möglichkeit zum Ausführen, führt aber nur die Schritte 1 bis 3 aus. Dies bedeutet, dass Thread 1 die Variable gelesen und die Addition abgeschlossen hat. Thread 1 wartet jetzt nur noch darauf, seinen neuen Wert von 150 auszuschreiben. Nachdem Thread 1 beendet wurde, wird Thread 2 vollständig ausgeführt. Dies bedeutet, dass der berechnete Wert (85) in die Variable Totalgeschrieben wurde. Schließlich erhält Thread 1 die Kontrolle wieder und beendet die Ausführung. Er schreibt seinen Wert (150) aus. Wenn Thread 1 abgeschlossen ist, ist der Wert von Total daher jetzt 150 anstelle von 85.

Sie können sehen, wie dies ein großes Problem sein könnte. Wenn es sich um ein Bankprogramm handelt, hat der Kunde Geld auf dem Konto, das nicht vorhanden sein sollte.

Dieser Fehler ist zufällig, da es möglich ist, dass Thread 1 seine Ausführung abschließen kann, bevor die Zeit auf dem Prozessor abläuft, und dann thread 2 mit der Ausführung beginnen kann. Wenn diese Ereignisse auftreten, tritt das Problem nicht auf. Die Threadausführung ist nicht deterministisch, daher können Sie den Zeitpunkt oder die Reihenfolge der Ausführung nicht steuern. Beachten Sie auch, dass die Threads im Laufzeitmodus und im Debugmodus unterschiedlich ausgeführt werden können. Außerdem können Sie sehen, dass der Fehler nicht auftritt, wenn Sie jeden Thread in Reihe ausführen. Diese Zufälligkeit erschwert das Auffinden und Debuggen dieser Fehler.

Um die Racebedingungen zu verhindern, können Sie freigegebene Variablen sperren, sodass jeweils nur ein Thread Zugriff auf die freigegebene Variable hat. Gehen Sie dies sparsam vor, denn wenn eine Variable in Thread 1 gesperrt ist und Thread 2 auch die Variable benötigt, wird die Ausführung von Thread 2 beendet, während Thread 2 darauf wartet, dass Thread 1 die Variable freigibt. (Weitere Informationen finden Sie SyncLock im Abschnitt Verweise dieses Artikels.)

Symptome für eine Racebedingung

Das häufigste Symptom einer Racebedingung sind unvorhersehbare Werte von Variablen, die von mehreren Threads gemeinsam genutzt werden. Dies ergibt sich aus der Unvorhersehbarkeit der Reihenfolge, in der die Threads ausgeführt werden. Irgendwann gewinnt ein Thread, und irgendwann gewinnt der andere Thread. Zu anderen Zeiten funktioniert die Ausführung ordnungsgemäß. Wenn jeder Thread separat ausgeführt wird, verhält sich der Variablenwert außerdem ordnungsgemäß.

Wenn Deadlocks auftreten

Ein Deadlock tritt auf, wenn zwei Threads jeweils eine andere Variable gleichzeitig sperren und dann versuchen, die Variable zu sperren, die der andere Thread bereits gesperrt hat. Infolgedessen beendet jeder Thread die Ausführung und wartet, bis der andere Thread die Variable freigibt. Da jeder Thread die variable enthält, die der andere Thread möchte, geschieht nichts, und die Threads bleiben deadlocked.

Details und Beispiele für Deadlocks

Der folgende Code verfügt über zwei -Objekte: LeftVal und RightVal:

  • Thread 1

    SyncLock LeftVal
        SyncLock RightVal
            'Perform operations on LeftVal and RightVal that require read and write.
        End SyncLock
    End SyncLock
    
  • Thread 2

    SyncLock RightVal
        SyncLock LeftVal
            'Perform operations on RightVal and LeftVal that require read and write.
        End SyncLock
    End SyncLock
    

Ein Deadlock tritt auf, wenn Thread 1 gesperrt LeftValwerden darf. Der Prozessor beendet die Ausführung von Thread 1 und beginnt mit der Ausführung von Thread 2. Thread 2 sperrt RightVal und versucht dann, zu sperren LeftVal. Da LeftVal gesperrt ist, wird Thread 2 beendet und wartet auf LeftVal die Freigabe. Da Thread 2 beendet ist, darf Thread 1 weiterhin ausgeführt werden. Thread 1 versucht zu sperren RightVal , kann aber nicht, da Thread 2 ihn gesperrt hat. Daher beginnt Thread 1 zu warten, bis RightVal verfügbar wird. Jeder Thread wartet auf den anderen Thread, da jeder Thread die Variable gesperrt hat, auf die der andere Thread wartet, und keiner der Threads entsperrt die Variable, die er hält.

Ein Deadlock tritt nicht immer auf. Wenn Thread 1 beide Sperren ausführt, bevor der Prozessor ihn beendet, kann Thread 1 seine Vorgänge ausführen und dann die freigegebene Variable entsperren. Nachdem Thread 1 die Variable entsperrt hat, kann Thread 2 die Ausführung wie erwartet fortsetzen.

Dieser Fehler scheint offensichtlich, wenn diese Codeausschnitte nebeneinander platziert werden, aber in der Praxis kann der Code in separaten Modulen oder Bereichen Ihres Codes angezeigt werden. Dies ist ein harter Fehler, der nachverfolgt werden muss, da aus demselben Code sowohl die richtige als auch die falsche Ausführung auftreten können.

Symptome für Deadlocks

Ein häufiges Symptom eines Deadlocks ist, dass das Programm oder die Gruppe von Threads nicht mehr reagiert. Dies wird auch als Hängen bezeichnet. Mindestens zwei Threads warten auf eine Variable, die der andere Thread gesperrt hat. Die Threads werden nicht fortgesetzt, da keiner der Threads seine Variable freigibt, bis er die andere Variable erhält. Das gesamte Programm kann hängen bleiben, wenn das Programm auf einen oder beide dieser Threads wartet, um die Ausführung abzuschließen.

Was ist ein Thread?

Prozesse werden verwendet, um die verschiedenen Anwendungen zu trennen, die zu einem bestimmten Zeitpunkt auf einem einzelnen Computer ausgeführt werden. Das Betriebssystem führt keine Prozesse aus, Threads jedoch. Ein Thread ist eine Ausführungseinheit. Das Betriebssystem weist einem Thread Prozessorzeit für die Ausführung der Threadaufgaben zu. Ein einzelner Prozess kann mehrere Ausführungsthreads enthalten. Jeder Thread verwaltet seine eigenen Ausnahmehandler, Planungsprioritäten und eine Reihe von Strukturen, die das Betriebssystem verwendet, um den Kontext des Threads zu speichern, wenn der Thread seine Ausführung während der Zuweisung zum Prozessor nicht abschließen kann. Der Kontext wird bis zum nächsten Mal beibehalten, wenn der Thread Prozessorzeit empfängt. Der Kontext enthält alle Informationen, die der Thread benötigt, um seine Ausführung nahtlos fortzusetzen. Diese Informationen umfassen die Prozessorregister des Threads und die Aufrufliste innerhalb des Adressraums des Hostprozesses.

References

Weitere Informationen finden Sie in der Visual Studio-Hilfe nach den folgenden Schlüsselwörtern:

  • SyncLock. Ermöglicht das Sperren eines Objekts. Wenn ein anderer Thread versucht, dasselbe Objekt zu sperren, wird es blockiert, bis der erste Thread freigegeben wird. Verwenden Sie SyncLock sorgfältig, da Probleme durch den Missbrauch von SyncLock entstehen können. Beispielsweise kann dieser Befehl Racebedingungen verhindern, aber Deadlocks verursachen.

  • InterLocked. Ermöglicht eine Auswahl von threadsicheren Vorgängen für grundlegende numerische Variablen.

Weitere Informationen finden Sie unter Threads und Threading.