Unter welchen Bedingungen kann ein Thread mehr als einmal gleichzeitig in einen Sperrbereich (Monitor) gelangen?

8

(Frage überarbeitet): Bisher enthalten alle Antworten einen einzelnen Thread, der linear in die Sperrregion zurückkehrt, beispielsweise durch Rekursion, wo Sie die Schritte eines einzelnen Threads, der zweimal in die Sperre eintritt, verfolgen können. Aber ist es irgendwie möglich, dass ein einzelner Thread (vielleicht vom ThreadPool, möglicherweise als Folge von Timer-Ereignissen oder asynchronen Ereignissen oder eines Threads, der in den Ruhezustand wechselt und in einem anderen Codeabschnitt separat aufgeweckt / wiederverwendet wird), irgendwie erzeugt wird zwei verschiedene Orte unabhängig voneinander, und damit in das Lock Re-Entrance-Problem laufen, wenn der Entwickler nicht erwartet, indem sie einfach ihren eigenen Code lesen?

In der ThreadPool-Klasse Bemerkungen ( klicken Sie hier ) scheinen die Anmerkungen zu sein schlagen vor, dass Schlaffäden wiederverwendet werden sollten, wenn sie nicht benutzt werden oder anderweitig durch Schlafen verschwendet werden.

Aber auf der Monitor.Enter Referenzseite ( klicken Sie hier ) sagen sie "Es ist legal, dass derselbe Thread mehr als einmal Enter aufruft, ohne dass er blockiert." Also denke ich, dass es etwas geben muss, was ich vorsichtig vermeiden sollte. Was ist es? Wie ist es möglich , dass ein einzelner Thread zweimal dieselbe Sperrregion eingibt?

Angenommen, Sie haben eine Sperrregion, die leider lange dauert. Dies kann zum Beispiel realistisch sein, wenn Sie auf Speicher zugreifen, der ausgelagert wurde (oder was auch immer). Der Thread in der gesperrten Region wird möglicherweise in den Ruhezustand versetzt. Wird derselbe Thread berechtigt, mehr Code auszuführen, der versehentlich in dieselbe Sperrregion gelangen könnte? Das Folgende führt bei meinen Tests NICHT dazu, dass mehrere Instanzen desselben Threads in denselben Sperrbereich gelangen.

Wie produziert man das Problem? Was genau müssen Sie vorsichtig sein zu vermeiden?

%Vor%     
Edward Ned Harvey 21.12.2012, 03:05
quelle

6 Antworten

1

ThreadPool-Threads können nicht anderswo wiederverwendet werden, nur weil sie schlafen gegangen sind; Sie müssen fertig sein, bevor sie wiederverwendet werden. Ein Thread, der eine lange Zeit in einer Sperrregion beansprucht, wird nicht dazu berechtigt, an einem anderen unabhängigen Kontrollpunkt mehr Code auszuführen. Die einzige Möglichkeit, den Wiedereinstieg von Sperren zu erleben, ist die Rekursion oder das Ausführen von Methoden oder Delegaten in einer Sperre, die die Sperre erneut eingeben.

    
Edward Ned Harvey 01.01.2013, 13:09
quelle
12

Angenommen, Sie haben eine Warteschlange mit Aktionen:

%Vor%

Angenommen, Queue<T> hat eine Methode Dequeue , die ein Bool zurückgibt, das angibt, ob die Warteschlange erfolgreich aus der Warteschlange entfernt werden konnte.

Und angenommen, Sie haben eine Schleife:

%Vor%

Offensichtlich dringt der Hauptfaden zweimal in M ​​ein; Dieser Code ist einspringend . Das heißt, es tritt selbst durch eine indirekte Rekursion ein.

Sieht Ihnen dieser Code unplausibel? Es sollte nicht. So funktioniert Windows . Jedes Fenster hat eine Nachrichtenwarteschlange, und wenn eine Nachrichtenwarteschlange "gepumpt" wird, werden Methoden aufgerufen, die diesen Nachrichten entsprechen. Wenn Sie auf eine Schaltfläche klicken, wird eine Nachricht in die Nachrichtenwarteschlange geschrieben. Wenn die Warteschlange gepumpt wird, wird der Click-Handler aufgerufen, der dieser Nachricht entspricht.

Es ist daher extrem üblich und äußerst gefährlich, Windows-Programme zu schreiben, bei denen eine Sperre einen Aufruf einer Methode enthält, die eine Nachrichtenschleife pumpt. Wenn Sie in diese Sperre als Ergebnis der Behandlung einer Nachricht an erster Stelle geraten sind, und wenn die Nachricht zweimal in der Warteschlange ist, dann wird der Code indirekt selbst eingeben, und das kann alle Arten von Verrücktheit verursachen.

Der Weg, dies zu eliminieren, ist (1) machen Sie niemals etwas auch nur etwas Kompliziertes in einer Sperre und (2) wenn Sie eine Nachricht bearbeiten, deaktivieren Sie den Handler , bis die Nachricht bearbeitet wird.

    
Eric Lippert 21.12.2012 06:45
quelle
5

Re-Entrance ist möglich, wenn Sie eine Struktur wie folgt haben:

%Vor%

Obwohl dies ein ziemlich einfaches Beispiel ist, ist es in vielen Szenarien möglich, in denen Sie interdependentes oder rekursives Verhalten haben.

Zum Beispiel:

  • ComponentA.Add (): sperrt ein gemeinsames 'ComponentA'-Objekt und fügt ein neues Element zu ComponentB hinzu.
  • ComponentB.OnNewItem (): neues Element löst Datenvalidierung für jedes Element in der Liste aus.
  • ComponentA.ValidateItem (): sperrt ein allgemeines "ComponentA" -Objekt, um das Element zu validieren.

Die erneute Eingabe desselben Threads in derselben Sperre ist erforderlich, um sicherzustellen, dass keine Deadlocks mit Ihrem eigenen Code auftreten.

    
user111013 21.12.2012 03:12
quelle
4

Eine der subtileren Möglichkeiten, die Sie in einen Sperrblock zurückholen können, sind GUI-Frameworks. Sie können beispielsweise Code für einen einzelnen UI-Thread (eine Form-Klasse) asynchron aufrufen

%Vor%

Natürlich setzt das auch eine Endlosschleife ein; Sie haben wahrscheinlich eine Bedingung, nach der Sie rekursen möchten. An diesem Punkt hätten Sie keine Endlosschleife.

Die Verwendung von lock ist keine gute Möglichkeit, Threads zu schlafen / zu erwecken. Ich würde einfach bestehende Frameworks wie Task Parallel Library (TPL) verwenden, um einfach abstrakte Aufgaben (siehe Task ) zu erstellen und das zugrundeliegende Framework handhabt das Erstellen neuer Threads und das Einschlafen bei Bedarf.

    
Peter Ritchie 21.12.2012 03:24
quelle
4

IMHO, eine erneute Eingabe einer Sperre ist nicht etwas, was Sie vermeiden müssen (angesichts des mentalen Modells vieler Leute von Sperren ist das im besten Fall gefährlich, siehe Bearbeiten unten). Der Punkt der Dokumentation ist zu erklären, dass ein Thread sich nicht mit Monitor.Enter blockieren kann. Dies ist bei allen Synchronisationsmechanismen, Frameworks und Sprachen nicht immer der Fall. Einige haben eine nichtregistrierte Synchronisation, in diesem Fall müssen Sie darauf achten, dass sich ein Thread nicht selbst blockiert. Worauf Sie achten müssen, ist immer Monitor.Exit für jeden Monitor.Enter Aufruf aufzurufen. Das Schlüsselwort lock erledigt dies automatisch für Sie.

Ein triviales Beispiel mit Wiedereintritt:

%Vor%

Der Thread hat die Sperre für dasselbe Objekt zweimal eingegeben, so dass er zweimal freigegeben werden muss. Normalerweise ist es nicht so offensichtlich und es gibt verschiedene Methoden, die sich gegenseitig aufrufen, die sich auf dasselbe Objekt synchronisieren. Der Punkt ist, dass Sie sich keine Gedanken darüber machen müssen, dass sich ein Thread selbst blockiert.

Das heißt, Sie sollten generell versuchen, die Zeit zu minimieren, die Sie benötigen, um eine Sperre zu halten. Das Erfassen einer Sperre ist im Gegensatz zu dem, was Sie hören können, nicht rechenintensiv (es liegt in der Größenordnung von einigen Nanosekunden). Sperrkonkurrenz ist was teuer ist.

Bearbeiten

Bitte lesen Sie die Kommentare von Eric unten für weitere Details, aber die Zusammenfassung ist, dass wenn Sie ein lock sehen, Ihre Interpretation davon sein sollte, dass "alle Aktivierungen dieses Codeblocks einem einzelnen Thread zugeordnet sind" und nicht , wie es allgemein interpretiert wird, "alle Aktivierungen dieses Codeblocks werden als eine einzige atomare Einheit ausgeführt".

Zum Beispiel:

%Vor%

Offensichtlich explodiert dieses Programm. Ohne lock ist es das gleiche Ergebnis. Die Gefahr besteht darin, dass das lock Sie in ein falsches Gefühl der Sicherheit führt, dass nichts den Zustand auf Ihnen zwischen der Initialisierung von j und der Auswertung von if ändern könnte. Das Problem ist, dass Sie (vielleicht unbeabsichtigt) Method in sich selbst rekursieren und das lock wird das nicht stoppen. Wie Eric in seiner Antwort betont, kann es sein, dass Sie das Problem erst bemerken, wenn jemand eines Tages zu viele Aktionen gleichzeitig in die Warteschlange stellt.

    
mike z 21.12.2012 05:53
quelle
0

Denken wir an etwas anderes als Rekursion.
In einigen Geschäftslogiken möchten sie das Verhalten der Synchronisierung steuern. Bei einem dieser Muster rufen sie Monitor.Enter irgendwo auf und möchten später an anderer Stelle Monitor.Exit aufrufen. Hier ist der Code, um die Idee zu bekommen:

%Vor%

Wenn Sie YourClass.PeformTest() aufrufen und einen lockCount größer als 1 erhalten, sind Sie erneut eingetreten. muss nicht gleichzeitig sein .
Wenn es nicht sicher für die Reentrancy ist, bleiben Sie in der foreach-Schleife stecken.
In dem Codeblock, in dem Monitor.Exit(intanceOfYourClass.yourLockObject) dir eine SynchronizationLockException liefert, liegt das daran, dass wir versuchen, Exit mehr als die eingegebenen Zeiten aufzurufen. Wenn Sie das Schlüsselwort lock verwenden möchten, würden Sie möglicherweise nicht auf diese Situation stoßen, außer direkt oder indirekt von rekursiven Aufrufen. Ich denke, deshalb wurde das Schlüsselwort lock bereitgestellt: Es verhindert, dass das Monitor.Exit auf eine unvorsichtige Art und Weise weggelassen wird.
Ich habe den Aufruf von Parallel.ForEach bemerkt, wenn du interessiert bist, kannst du es zum Spaß testen.

Um den Code zu testen, ist .Net Framework 4.0 die kleinste Anforderung, und folgende zusätzliche Namensräume sind ebenfalls erforderlich:

%Vor%

Viel Spaß.

    
Ken Kin 31.12.2012 18:59
quelle