AKTUALISIEREN
Ab C # 6 lautet die Antwort auf diese Frage:
SomeEvent?.Invoke(this, e);
Ich höre / lese häufig den folgenden Rat:
Erstellen Sie immer eine Kopie eines Ereignisses, bevor Sie es überprüfen null
und auslösen. Dadurch wird ein potenzielles Problem beim Threading beseitigt, bei dem das Ereignis null
genau zwischen dem Ort, an dem Sie auf Null prüfen, und dem Ort, an dem Sie das Ereignis auslösen, auftritt:
// Copy the event delegate before checking/calling
EventHandler copy = TheEvent;
if (copy != null)
copy(this, EventArgs.Empty); // Call any handlers on the copied list
Aktualisiert : Ich habe beim Lesen über Optimierungen gedacht, dass das Ereignismitglied möglicherweise auch volatil sein muss, aber Jon Skeet gibt in seiner Antwort an, dass die CLR die Kopie nicht optimiert.
Damit dieses Problem überhaupt auftritt, muss ein anderer Thread Folgendes getan haben:
// Better delist from event - don't want our handler called from now on:
otherObject.TheEvent -= OnTheEvent;
// Good, now we can be certain that OnTheEvent will not run...
Die tatsächliche Reihenfolge könnte diese Mischung sein:
// Copy the event delegate before checking/calling
EventHandler copy = TheEvent;
// Better delist from event - don't want our handler called from now on:
otherObject.TheEvent -= OnTheEvent;
// Good, now we can be certain that OnTheEvent will not run...
if (copy != null)
copy(this, EventArgs.Empty); // Call any handlers on the copied list
Der Punkt ist, dass OnTheEvent
läuft, nachdem der Autor sich abgemeldet hat, und sie sich nur speziell abgemeldet haben, um dies zu vermeiden. Sicherlich ist eine benutzerdefinierte Ereignisimplementierung mit entsprechender Synchronisation in den add
und remove
Accessoren wirklich erforderlich . Darüber hinaus besteht das Problem möglicher Deadlocks, wenn eine Sperre gehalten wird, während ein Ereignis ausgelöst wird.
Also ist das Cargo Cult Programming ? Es scheint so - viele Leute müssen diesen Schritt unternehmen, um ihren Code vor mehreren Threads zu schützen, obwohl Ereignisse in Wirklichkeit meiner Meinung nach viel mehr Sorgfalt erfordern, bevor sie als Teil eines Multithread-Designs verwendet werden können . Folglich können Personen, die diese zusätzliche Sorgfalt nicht anwenden, diesen Rat genauso gut ignorieren - es ist einfach kein Problem für Single-Thread-Programme, und angesichts des Fehlens des volatile
meisten Online-Beispielcodes kann der Ratschlag keine enthalten Wirkung überhaupt.
(Und ist es nicht viel einfacher, nur das Leerzeichen delegate { }
in der Mitgliedererklärung zuzuweisen, damit Sie überhaupt nicht danach suchen müssen null
?)
Aktualisiert:Falls es nicht klar war, habe ich die Absicht des Ratschlags verstanden - unter allen Umständen eine Nullreferenzausnahme zu vermeiden. Mein Punkt ist, dass diese spezielle Nullreferenzausnahme nur auftreten kann, wenn ein anderer Thread aus dem Ereignis entfernt wird. Der einzige Grund dafür besteht darin, sicherzustellen, dass keine weiteren Anrufe über dieses Ereignis empfangen werden, was mit dieser Technik eindeutig NICHT erreicht wird . Sie würden eine Rennbedingung verbergen - es wäre besser, sie zu enthüllen! Diese Null-Ausnahme hilft, einen Missbrauch Ihrer Komponente zu erkennen. Wenn Sie möchten, dass Ihre Komponente vor Missbrauch geschützt wird, können Sie dem Beispiel von WPF folgen. Speichern Sie die Thread-ID in Ihrem Konstruktor und lösen Sie eine Ausnahme aus, wenn ein anderer Thread versucht, direkt mit Ihrer Komponente zu interagieren. Oder implementieren Sie eine wirklich threadsichere Komponente (keine leichte Aufgabe).
Ich behaupte also, dass das bloße Ausführen dieser Kopier- / Überprüfungssprache eine Frachtkultprogrammierung ist, die Ihrem Code Chaos und Rauschen hinzufügt. Um sich tatsächlich vor anderen Threads zu schützen, ist viel mehr Arbeit erforderlich.
Update als Antwort auf die Blog-Beiträge von Eric Lippert:
Es gibt also eine wichtige Sache, die ich an Event-Handlern vermisst habe: "Event-Handler müssen robust sein, wenn sie auch nach dem Abbestellen des Events aufgerufen werden", und deshalb müssen wir uns natürlich nur um die Möglichkeit des Events kümmern delegieren sein null
. Ist diese Anforderung an Event-Handler irgendwo dokumentiert?
Und so: "Es gibt andere Möglichkeiten, um dieses Problem zu lösen. Zum Beispiel das Initialisieren des Handlers mit einer leeren Aktion, die niemals entfernt wird. Eine Nullprüfung ist jedoch das Standardmuster."
Das einzige verbleibende Fragment meiner Frage ist also, warum das "Standardmuster" explizit auf Null überprüft wird. Die Alternative, den leeren Delegierten zuzuweisen, muss nur = delegate {}
zur Ereigniserklärung hinzugefügt werden, und dies beseitigt diese kleinen Haufen stinkender Zeremonien an jedem Ort, an dem das Ereignis ausgelöst wird. Es wäre einfach sicherzustellen, dass der leere Delegat billig zu instanziieren ist. Oder fehlt mir noch etwas?
Sicherlich muss es sein, dass (wie Jon Skeet vorgeschlagen hat) dies nur ein .NET 1.x-Rat ist, der nicht ausgestorben ist, wie es 2005 hätte tun sollen?
quelle
EventName(arguments)
, den Delegaten des Ereignisses bedingungslos aufzurufen, anstatt den Delegaten nur dann aufzurufen, wenn er nicht null ist (nichts tun, wenn null).Antworten:
Die JIT darf die Optimierung, über die Sie im ersten Teil sprechen, aufgrund der Bedingung nicht durchführen. Ich weiß, dass dies vor einiger Zeit als Gespenst angesprochen wurde, aber es ist nicht gültig. (Ich habe es vor einiger Zeit entweder mit Joe Duffy oder Vance Morrison überprüft. Ich kann mich nicht erinnern, welche.)
Ohne den flüchtigen Modifikator ist die möglicherweise erstellte lokale Kopie veraltet, aber das ist alles. Es wird keine verursachen
NullReferenceException
.Und ja, es gibt sicherlich eine Rennbedingung - aber es wird immer eine geben. Angenommen, wir ändern einfach den Code in:
Angenommen, die Aufrufliste für diesen Delegaten enthält 1000 Einträge. Es ist durchaus möglich, dass die Aktion am Anfang der Liste ausgeführt wurde, bevor ein anderer Thread einen Handler am Ende der Liste abmeldet. Dieser Handler wird jedoch weiterhin ausgeführt, da es sich um eine neue Liste handelt. (Die Delegierten sind unveränderlich.) Soweit ich sehen kann, ist dies unvermeidlich.
Die Verwendung eines leeren Delegaten vermeidet zwar die Nichtigkeitsprüfung, behebt jedoch nicht die Rennbedingung. Es wird auch nicht garantiert, dass Sie immer den neuesten Wert der Variablen "sehen".
quelle
Ich sehe viele Leute, die sich der Erweiterungsmethode nähern ...
Das gibt Ihnen eine schönere Syntax, um das Ereignis auszulösen ...
Außerdem wird die lokale Kopie entfernt, da sie zum Zeitpunkt des Methodenaufrufs erfasst wird.
quelle
"Warum wird das 'Standardmuster' explizit auf Null überprüft?"
Ich vermute, der Grund dafür könnte sein, dass die Nullprüfung leistungsfähiger ist.
Wenn Sie Ihre Ereignisse beim Erstellen immer mit einem leeren Delegaten abonnieren, entstehen einige Gemeinkosten:
(Beachten Sie, dass UI-Steuerelemente häufig eine große Anzahl von Ereignissen aufweisen, von denen die meisten nie abonniert werden. Wenn Sie für jedes Ereignis einen Dummy-Abonnenten erstellen und dann aufrufen müssen, ist dies wahrscheinlich ein erheblicher Leistungseinbruch.)
Ich habe einige flüchtige Leistungstests durchgeführt, um die Auswirkungen des Ansatzes "Abonnieren-Leer-Delegieren" zu ermitteln. Hier sind meine Ergebnisse:
Beachten Sie, dass für den Fall von null oder einem Abonnenten (häufig bei UI-Steuerelementen, bei denen zahlreiche Ereignisse vorhanden sind) das mit einem leeren Delegaten vorinitialisierte Ereignis erheblich langsamer ist (über 50 Millionen Iterationen ...).
Weitere Informationen und Quellcode finden Sie in diesem Blogbeitrag zur Sicherheit von .NET-Ereignisaufruf-Threads , den ich am Tag vor der Beantwortung dieser Frage veröffentlicht habe (!).
(Mein Testaufbau ist möglicherweise fehlerhaft. Laden Sie den Quellcode herunter und überprüfen Sie ihn selbst. Jedes Feedback wird sehr geschätzt.)
quelle
Delegate.Combine
/Delegate.Remove
Paar zeitlich festzulegen. Wenn einer wiederholt dieselbe Delegateninstanz hinzufügt und entfernt, sind die Kostenunterschiede zwischen den Fällen besonders ausgeprägt, da erCombine
sich schnell in Sonderfällen verhält, wenn eines der Argumente istnull
(nur das andere zurückgeben), undRemove
sehr schnell ist, wenn die beiden Argumente vorliegen gleich (nur null zurückgeben).Ich habe diese Lektüre wirklich genossen - nicht! Auch wenn ich es brauche, um mit der C # -Funktion namens events zu arbeiten!
Warum nicht im Compiler beheben? Ich weiß, dass es MS-Leute gibt, die diese Beiträge lesen, also bitte nicht flammen!
1 - das Null-Problem ) Warum sollten Ereignisse nicht in erster Linie .Empty statt null sein? Wie viele Codezeilen würden für die Nullprüfung gespeichert oder müssten
= delegate {}
auf die Deklaration geklebt werden? Lassen Sie den Compiler den leeren Fall behandeln, IE tut nichts! Wenn es für den Ersteller des Ereignisses wichtig ist, können sie nach .Empty suchen und alles tun, was ihnen wichtig ist! Ansonsten sind alle Nullprüfungen / Delegatenzusätze Hacks um das Problem!Ehrlich gesagt bin ich es leid, dies bei jedem Event tun zu müssen - auch bekannt als Boilerplate-Code!
2 - das Problem mit den Rennbedingungen ) Ich habe Erics Blog-Beitrag gelesen und bin damit einverstanden, dass der H (Handler) damit umgehen soll, wenn er sich selbst dereferenziert, aber kann das Ereignis nicht unveränderlich / threadsicher gemacht werden? IE, setzen Sie ein Sperrflag bei seiner Erstellung, damit es bei jedem Aufruf alle Abonnenten und Abonnenten sperrt, während es ausgeführt wird?
Schlussfolgerung ,
Sollen moderne Sprachen solche Probleme nicht für uns lösen?
quelle
Mit C # 6 und höher kann der Code mithilfe eines neuen
?.
Operators wie folgt vereinfacht werden :TheEvent?.Invoke(this, EventArgs.Empty);
Hier ist die MSDN-Dokumentation.
quelle
Laut Jeffrey Richter im Buch CLR via C # ist die richtige Methode:
Weil es eine Referenzkopie erzwingt. Weitere Informationen finden Sie in seinem Abschnitt "Veranstaltung" im Buch.
quelle
Interlocked.CompareExchange
würde scheitern , wenn es irgendwie eine Null übergeben wurderef
, aber das ist nicht dasselbe wie ein übergeben wirdref
an einen Lagerort (zBNewMail
) , die vorhanden ist, und die zunächst hält eine Nullreferenz.Ich habe dieses Entwurfsmuster verwendet, um sicherzustellen, dass Ereignishandler nicht ausgeführt werden, nachdem sie abgemeldet wurden. Bisher funktioniert es ziemlich gut, obwohl ich noch kein Leistungsprofil ausprobiert habe.
Ich arbeite heutzutage hauptsächlich mit Mono für Android, und Android scheint es nicht zu mögen, wenn Sie versuchen, eine Ansicht zu aktualisieren, nachdem ihre Aktivität in den Hintergrund gestellt wurde.
quelle
Bei dieser Praxis geht es nicht darum, eine bestimmte Reihenfolge von Operationen durchzusetzen. Es geht eigentlich darum, eine Nullreferenzausnahme zu vermeiden.
Die Argumentation hinter Menschen, die sich für die Nullreferenzausnahme und nicht für die Rassenbedingung interessieren, würde einige gründliche psychologische Untersuchungen erfordern. Ich denke, das hat etwas damit zu tun, dass die Behebung des Nullreferenzproblems viel einfacher ist. Sobald dies behoben ist, hängen sie ein großes "Mission Accomplished" -Banner an ihren Code und entpacken ihren Fluganzug.
Hinweis: Um den Rennzustand zu beheben, muss wahrscheinlich eine synchrone Flaggenspur verwendet werden, ob der Handler ausgeführt werden soll
quelle
Unload
Ereignis) unmöglich machen, die Abonnements eines Objekts für andere Ereignisse sicher zu kündigen. Böse. Besser ist es einfach zu sagen, dass Anfragen zum Abbestellen von Ereignissen dazu führen, dass Ereignisse "irgendwann" abgemeldet werden, und dass Abonnenten von Ereignissen prüfen sollten, wann sie aufgerufen werden, ob sie etwas Nützliches tun können.Also bin ich etwas spät zur Party hier. :) :)
Betrachten Sie dieses Szenario für die Verwendung von null anstelle des Null-Objektmusters zur Darstellung von Ereignissen ohne Abonnenten. Sie müssen ein Ereignis aufrufen, aber das Erstellen des Objekts (EventArgs) ist nicht trivial, und im allgemeinen Fall hat Ihr Ereignis keine Abonnenten. Es wäre für Sie von Vorteil, wenn Sie Ihren Code optimieren könnten, um zu überprüfen, ob Sie überhaupt Abonnenten hatten, bevor Sie den Verarbeitungsaufwand für die Erstellung der Argumente und den Aufruf des Ereignisses aufbringen.
In diesem Sinne lautet eine Lösung: "Nun, Null-Abonnenten werden durch Null dargestellt." Führen Sie dann einfach die Nullprüfung durch, bevor Sie Ihren teuren Vorgang ausführen. Ich nehme an, eine andere Möglichkeit, dies zu tun, wäre gewesen, eine Count-Eigenschaft für den Delegate-Typ zu haben, sodass Sie die teure Operation nur ausführen würden, wenn myDelegate.Count> 0. Die Verwendung einer Count-Eigenschaft ist ein hübsches Muster, das das ursprüngliche Problem löst Es hat auch die nette Eigenschaft, aufgerufen werden zu können, ohne eine NullReferenceException zu verursachen.
Beachten Sie jedoch, dass Delegaten, da sie Referenztypen sind, null sein dürfen. Vielleicht gab es einfach keine gute Möglichkeit, diese Tatsache unter der Decke zu verbergen und nur das Nullobjektmuster für Ereignisse zu unterstützen, sodass die Alternative die Entwickler möglicherweise gezwungen hat, sowohl nach Null- als auch nach Null-Abonnenten zu suchen. Das wäre noch hässlicher als die aktuelle Situation.
Hinweis: Dies ist reine Spekulation. Ich bin nicht mit den .NET-Sprachen oder der CLR befasst.
quelle
+=
und-=
. Innerhalb der Klasse umfassen die zulässigen Operationen auch den Aufruf (mit integrierter Nullprüfung), das Testen gegennull
oder das Setzen aufnull
. Für alles andere müsste man den Delegaten verwenden, dessen Name der Ereignisname mit einem bestimmten Präfix oder Suffix wäre.Bei Single-Threaded-Anwendungen ist dies kein Problem.
Wenn Sie jedoch eine Komponente erstellen, die Ereignisse verfügbar macht, gibt es keine Garantie dafür, dass ein Verbraucher Ihrer Komponente kein Multithreading ausführt. In diesem Fall müssen Sie sich auf das Schlimmste vorbereiten.
Die Verwendung des leeren Delegaten löst das Problem, führt jedoch bei jedem Aufruf des Ereignisses zu einem Leistungseinbruch und kann möglicherweise Auswirkungen auf die GC haben.
Sie haben Recht, dass der Verbraucher versucht, sich abzumelden, damit dies geschieht. Wenn er es jedoch über die temporäre Kopie hinaus geschafft hat, berücksichtigen Sie die Nachricht, die bereits übertragen wird.
Wenn Sie die temporäre Variable nicht verwenden und den leeren Delegaten nicht verwenden und sich jemand abmeldet, erhalten Sie eine Nullreferenzausnahme, die schwerwiegend ist. Ich denke, die Kosten sind es wert.
quelle
Ich habe dies nie wirklich als großes Problem angesehen, da ich mich im Allgemeinen nur bei statischen Methoden (usw.) meiner wiederverwendbaren Komponenten vor dieser Art von potenziellem Threading-Fehler schütze und keine statischen Ereignisse mache.
Mache ich es falsch
quelle
Verdrahten Sie alle Ihre Veranstaltungen beim Bau und lassen Sie sie in Ruhe. Das Design der Delegate-Klasse kann möglicherweise keine andere Verwendung korrekt verarbeiten, wie ich im letzten Absatz dieses Beitrags erläutern werde.
Zunächst einmal gibt es keinen Sinn, abfangen eine Ereignisbenachrichtigung , wenn Ihr Event - Handler muss bereits eine synchronisierte Entscheidung darüber , ob / wie man reagiert auf die Meldung .
Alles, was benachrichtigt werden kann, sollte benachrichtigt werden. Wenn Ihre Ereignishandler die Benachrichtigungen ordnungsgemäß verarbeiten (dh sie haben Zugriff auf einen autorisierenden Anwendungsstatus und antworten nur bei Bedarf), ist es in Ordnung, sie jederzeit zu benachrichtigen und darauf zu vertrauen, dass sie ordnungsgemäß reagieren.
Das einzige Mal, wenn ein Handler nicht benachrichtigt werden sollte, dass ein Ereignis aufgetreten ist, ist, wenn das Ereignis tatsächlich nicht aufgetreten ist! Wenn Sie also nicht möchten, dass ein Handler benachrichtigt wird, beenden Sie die Generierung der Ereignisse (dh deaktivieren Sie das Steuerelement oder was auch immer für die Erkennung und Aktivierung des Ereignisses verantwortlich ist).
Ehrlich gesagt denke ich, dass die Delegiertenklasse nicht mehr zu retten ist. Die Fusion / der Übergang zu einem MulticastDelegate war ein großer Fehler, da die (nützliche) Definition eines Ereignisses von etwas, das zu einem bestimmten Zeitpunkt passiert, in etwas geändert wurde, das über einen bestimmten Zeitraum hinweg passiert. Eine solche Änderung erfordert einen Synchronisationsmechanismus, der sie logisch auf einen einzigen Moment reduzieren kann, aber dem MulticastDelegate fehlt ein solcher Mechanismus. Die Synchronisierung sollte die gesamte Zeitspanne oder den Zeitpunkt des Ereignisses umfassen, damit eine Anwendung, sobald sie die synchronisierte Entscheidung getroffen hat, mit der Behandlung eines Ereignisses zu beginnen, die vollständige (transaktionale) Behandlung beendet. Mit der Black Box, der MulticastDelegate / Delegate-Hybridklasse, ist dies nahezu unmöglichVerwenden Sie einen einzelnen Abonnenten und / oder implementieren Sie Ihre eigene Art von MulticastDelegate mit einem Synchronisationshandle, das entfernt werden kann, während die Handlerkette verwendet / geändert wird . Ich empfehle dies, da die Alternative darin besteht, Synchronisation / Transaktionsintegrität redundant in all Ihren Handlern zu implementieren, was lächerlich / unnötig komplex wäre.
quelle
Bitte schauen Sie hier: http://www.danielfortunov.com/software/%24daniel_fortunovs_adventures_in_software_development/2009/04/23/net_event_invocation_thread_safety Dies ist die richtige Lösung und sollte immer anstelle aller anderen Problemumgehungen verwendet werden.
„Sie können sicherstellen, dass die interne Aufrufliste immer mindestens ein Mitglied enthält, indem Sie sie mit einer anonymen Do-Nothing-Methode initialisieren. Da keine externe Partei einen Verweis auf die anonyme Methode haben kann, kann keine externe Partei die Methode entfernen, sodass der Delegat niemals null sein wird. “- Programming .NET Components, 2nd Edition, von Juval Löwy
quelle
Ich glaube nicht, dass die Frage auf den Typ c # "event" beschränkt ist. Wenn Sie diese Einschränkung aufheben, können Sie das Rad ein wenig neu erfinden und etwas in diese Richtung tun.
Ereignis-Thread sicher anheben - Best Practice
quelle
Vielen Dank für eine nützliche Diskussion. Ich habe kürzlich an diesem Problem gearbeitet und die folgende Klasse erstellt, die etwas langsamer ist, aber es ermöglicht, Aufrufe von entsorgten Objekten zu vermeiden.
Der Hauptpunkt hierbei ist, dass die Aufrufliste geändert werden kann, selbst wenn ein Ereignis ausgelöst wird.
Und die Verwendung ist:
Prüfung
Ich habe es folgendermaßen getestet. Ich habe einen Thread, der Objekte wie dieses erstellt und zerstört:
In einem
Bar
Konstruktor (einem Listener-Objekt), den ich abonniereSomeEvent
(der wie oben gezeigt implementiert ist) und melde inDispose
:Außerdem habe ich einige Threads, die Ereignisse in einer Schleife auslösen.
Alle diese Aktionen werden gleichzeitig ausgeführt: Viele Listener werden erstellt und zerstört, und gleichzeitig wird ein Ereignis ausgelöst.
Wenn es Rennbedingungen gab, sollte ich eine Nachricht in einer Konsole sehen, aber sie ist leer. Aber wenn ich wie gewohnt clr-Ereignisse verwende, sehe ich es voller Warnmeldungen. Daraus kann ich schließen, dass es möglich ist, thread-sichere Ereignisse in c # zu implementieren.
Was denken Sie?
quelle
disposed = true
dies zuvorfoo.SomeEvent -= Handler
in Ihrer Testanwendung passiert und ein falsches Positiv erzeugt wird. Abgesehen davon gibt es einige Dinge, die Sie möglicherweise ändern möchten. Sie möchten wirklichtry ... finally
für die Schlösser verwenden - dies hilft Ihnen dabei, dies nicht nur threadsicher, sondern auch abbruchsicher zu machen. Ganz zu schweigen davon, dass man das dann albern loswerden könntetry catch
. Und Sie überprüfen nicht den Delegierten, derAdd
/ übergeben wurdeRemove
- es könnte seinnull
(Sie sollten sofort inAdd
/ werfenRemove
).