(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%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.
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.
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:
Die erneute Eingabe desselben Threads in derselben Sperre ist erforderlich, um sicherzustellen, dass keine Deadlocks mit Ihrem eigenen Code auftreten.
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.
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.
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:
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:
Viel Spaß.
Tags und Links .net c# thread-safety concurrency locking