Gibt es eine .NET-Klasse, um das zu tun, was ManualResetEvent.PulseAll()
tun würde (wenn es existiert)?
Ich muss eine Reihe von Threads, die auf dasselbe Signal warten, atomar freigeben. (Ich mache mir keine Sorgen wegen "Thread-Stampedes" für meine beabsichtigte Verwendung.)
Sie können dafür kein ManualResetEvent
verwenden. Zum Beispiel, wenn Sie:
Dann werden überhaupt keine Threads mehr auf Signal freigegeben.
Wenn Sie Thread.Sleep(5)
zwischen den Aufrufen Set()
und Reset()
setzen, werden einige, aber nicht alle wartenden Threads alle freigegeben. Wenn Sie den Ruhezustand auf 10 ms erhöhen, können alle Threads freigegeben werden. (Dies wurde mit 20 Threads getestet.)
Es ist natürlich nicht akzeptabel, Thread.Sleep()
hinzuzufügen, um dies zu erreichen.
Das ist jedoch leicht genug mit Monitor.PulseAll()
zu tun und ich habe eine kleine Klasse dafür geschrieben.
(Der Grund dafür, dass ich eine Klasse geschrieben habe, ist, dass wir herausgefunden haben, dass die Logik mit Monitor zwar ziemlich einfach ist, aber nicht offensichtlich genug ist, um eine solche Klasse zur Vereinfachung der Benutzung zu haben.)
Meine Frage ist einfach: Gibt es schon eine Klasse in .Net, um das zu tun?
Als Referenz folgt hier die nackte Version meines " ManualResetEvent.PulseAll()
" - Äquivalents:
Hier ist ein Beispielprogramm, das demonstriert, dass keine wartenden Threads freigegeben werden, wenn Sie zwischen Set () und Reset () nicht schlafen:
%Vor%Sie können ein Barriere Objekt. Es ermöglicht die Ausführung einer unbestimmten Anzahl von Tasks und wartet darauf, dass alle anderen diesen Punkt erreichen.
Und Sie können ähnlich wie WaitGroup in Go verwenden , wenn Sie nicht wissen, welche Aufgaben von welchen Code-Blöcken als spezifische Arbeitseinheit zu arbeiten beginnen.
Version 1
Maximale Klarheit: ein neuer ManualResetEvent
wird zu Beginn jedes PulseAll
Zykluses eifrig installiert.
Version 2
Diese Version vermeidet das Erstellen des internen Ereignisses für PulseAll
Zyklen, die ohne Kellner abgeschlossen werden. Der / die erste (n) Kellner pro Zyklus geben ein optimistisches Rennen ohne Sperre ein, um ein einzelnes gemeinsames Ereignis zu erstellen und atomar zu installieren.
Version 3
Diese Version eliminiert Zuteilungen pro Zyklus, indem sie zwei beständige ManualResetEvent
-Objekte zuweist und zwischen ihnen umkehrt. Dies ändert die Semantik gegenüber den obigen Beispielen leicht wie folgt:
Erstens bedeutet das Recyceln derselben zwei Sperren, dass Ihre PulseAll
Zyklen lang genug sein müssen, damit alle Kellner die vorherige Sperre löschen können. Andernfalls, wenn Sie PulseAll
zweimal in schneller Folge aufrufen, werden alle wartenden Threads, die mutmaßlich vom vorherigen PulseAll
-Aufruf freigegeben wurden, aber vom OS noch nicht geplant - kann auch für den neuen Zyklus wieder blockiert werden. Ich erwähne das meist als theoretische Überlegung, weil es ein Problem ist, wenn Sie nicht eine extreme Anzahl von Threads bei Pulszyklen im Sub-Mikrosekunden-Bereich blockieren. Sie können entscheiden, ob diese Bedingung für Ihre Situation relevant ist oder nicht. Wenn dies der Fall ist oder Sie unsicher oder vorsichtig sind, können Sie immer Version 1 oder Version 2 oben verwenden, für die diese Einschränkung nicht gilt.
Auch "strittig" anders (aber siehe unten, warum dieser zweite Punkt nachweislich irrelevant ist) In dieser Version werden Aufrufe von PulseAll
, die im Wesentlichen als simultan angesehen werden, zusammengeführt, dh alle außer einem von mehreren "gleichzeitige" Anrufer werden zu NOPs . Ein solches Verhalten ist nicht ohne Beispiel (siehe "Bemerkungen" hier ) und kann je nach Anwendung wünschenswert sein.
Beachten Sie, dass der letztere Punkt als eine legitime Design-Wahl betrachtet werden muss, im Gegensatz zu einem Bug, einem theoretischen Fehler oder einem Nebenläufigkeitsfehler. Dies liegt daran, dass Pulse -Sperren in Situationen mit mehreren gleichzeitigen PulseAll
inhärent mehrdeutig sind: Insbesondere gibt es keine Möglichkeit, zu beweisen, dass ein Kellner, der nicht durch den einzelnen, angegebenen Pulser freigegeben wird, notwendigerweise freigegeben wird durch einen der anderen zusammengeführten / geteilten Impulse entweder.
Anders gesagt, diese Art von Sperre ist nicht dazu gedacht, die PulseAll
-Anrufer atomar zu serialisieren, und tatsächlich kann es auch nicht sein, weil es immer möglich ist, einen "simultanen" Impuls zu überspringen unabhängig kommen und gehen, auch wenn sie nach der Zeit des verschmolzenen Pulses ganz und gar noch "pulsieren" vor der Ankunft des Kellners (der nicht gepulst wird).
Abschließend ein paar allgemeine Beobachtungen. Ein wichtiges wiederkehrendes Thema hier und in dieser Art von Code ist im Allgemeinen, dass ManualResetEvent
nicht in den signalisierten Status geändert werden darf (d. H. Durch Aufrufen von Set
) , solange es noch öffentlich sichtbar ist . Im obigen Code verwenden wir Interlocked.Exchange
, um die Identität der aktiven Sperre in 'cur' (in diesem Fall durch sofortiges Tauschen in der Alternative) automatisch zu ändern und dies vor Set
ist entscheidend, um zu gewährleisten, dass zu diesem ManualResetEvent
keine neuen Kellner hinzugefügt werden können, die über diejenigen hinausgehen, die zum Zeitpunkt des Swappings bereits gesperrt waren.
Erst nach diesem Austausch ist es sicher, diese wartenden Threads durch Aufruf von Set
auf unserer (jetzt-) privaten Kopie freizugeben. Wenn wir Set
auf dem ManualResetEvent
noch während der Veröffentlichung nennen würden, wäre es einem spät ankommenden Kellner möglich, der den spontanen Impuls tatsächlich verpasst hat, trotzdem die offene Schleuse zu sehen und ohne durchzufahren nächste, wie per Definition erforderlich.
Interessanterweise bedeutet dies, dass, obwohl es sich intuitiv so anfühlen mag, dass der genaue Zeitpunkt, zu dem das "Pulsieren" auftritt, mit Set
zusammenfällt, tatsächlich wird es früher richtig gesagt, im Moment Die Interlocked.Exchange
, denn das ist die Aktion, die streng die Vorher-Nachher-Cut-off-Zeit festlegt und die definitive Anzahl der Kellner (falls vorhanden), die entlassen werden sollen, abschließt.
So müssen Kellner, die den Cut-Off verpassen und unmittelbar danach ankommen, nur das Event sehen und blockieren, das jetzt für den nächsten Zyklus bestimmt ist, und dies gilt auch für even wenn der momentane Zyklus noch nicht signalisiert wurde, noch einer seiner wartenden Threads freigegeben ist, alle wie für die Korrektheit des "momentanen" Pulsierens erforderlich.
Tags und Links c# multithreading