C # -Ereignisse und Thread-Sicherheit


236

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 nullund auslösen. Dadurch wird ein potenzielles Problem beim Threading beseitigt, bei dem das Ereignis nullgenau 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 OnTheEventlä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 addund removeAccessoren 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 volatilemeisten 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?


4
Diese Frage wurde vor einiger Zeit in einer internen Diskussion gestellt. Ich habe vor, dies seit einiger Zeit zu bloggen. Mein Beitrag zu diesem Thema ist hier: Events and Races
Eric Lippert

3
Stephen Cleary hat einen CodeProject-Artikel, der dieses Problem untersucht, und er kommt zu dem Schluss, dass es keine allgemeine, "thread-sichere" Lösung gibt. Grundsätzlich ist es Sache des Ereignisaufrufers, sicherzustellen, dass der Delegat nicht null ist, und es ist Sache des Ereignishandlers, das Aufrufen nach dem Abbestellen zu verarbeiten.
Rkagerer

3
@rkagerer - tatsächlich muss das zweite Problem manchmal vom Ereignishandler behandelt werden, auch wenn keine Threads beteiligt sind. Dies kann passieren, wenn ein Ereignishandler einen anderen Handler anweist, sich von dem aktuell behandelten Ereignis abzumelden, dieser zweite Abonnent das Ereignis jedoch trotzdem erhält (da es während der Bearbeitung abgemeldet wurde).
Daniel Earwicker

3
Das Hinzufügen eines Abonnements zu einem Ereignis mit null Abonnenten, das Entfernen des einzigen Abonnements für das Ereignis, das Aufrufen eines Ereignisses mit null Abonnenten und das Aufrufen eines Ereignisses mit genau einem Abonnenten sind viel schnellere Vorgänge als das Hinzufügen / Entfernen / Aufrufen von Szenarien mit einer anderen Anzahl von Abonnenten. Das Hinzufügen eines Dummy-Delegaten verlangsamt den allgemeinen Fall. Das eigentliche Problem mit C # besteht darin, dass die Ersteller beschlossen haben EventName(arguments), den Delegaten des Ereignisses bedingungslos aufzurufen, anstatt den Delegaten nur dann aufzurufen, wenn er nicht null ist (nichts tun, wenn null).
Supercat

Antworten:


100

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:

TheEvent(this, EventArgs.Empty);

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".


4
Joe Duffys "Concurrent Programming unter Windows" behandelt den Aspekt der JIT-Optimierung und des Speichermodells der Frage. siehe code.logos.com/blog/2008/11/events_and_threads_part_4.html
Bradley Grainger

2
Ich habe dies aufgrund des Kommentars akzeptiert, dass der "Standard" -Rat vor C # 2 lautet, und ich höre niemanden, der dem widerspricht. Wenn es nicht wirklich teuer ist, Ihre Ereignisargumente zu instanziieren, setzen Sie einfach '= delegate {}' am Ende Ihrer Ereignisdeklaration und rufen Sie Ihre Ereignisse direkt auf, als wären sie Methoden. Weisen Sie ihnen niemals Null zu. (Die anderen Dinge, die ich eingebracht habe, um sicherzustellen, dass ein Handler nach dem Delisting nicht aufgerufen wird, waren irrelevant und können selbst bei Code mit einem Thread nicht sichergestellt werden, z. B. wenn Handler 1 Handler 2 zum Delist auffordert, wird Handler 2 weiterhin aufgerufen weiter.)
Daniel Earwicker

2
Der einzige Problemfall (wie immer) sind Strukturen, bei denen Sie nicht sicherstellen können, dass sie mit anderen als Nullwerten in ihren Mitgliedern instanziiert werden. Aber Strukturen saugen.
Daniel Earwicker

1
Informationen zum leeren Delegaten finden Sie auch in dieser Frage: stackoverflow.com/questions/170907/… .
Vladimir

2
@Tony: Grundsätzlich gibt es immer noch eine Race-Bedingung zwischen dem Abonnieren / Abbestellen und dem ausgeführten Delegierten. Ihr Code (nachdem Sie ihn nur kurz durchgesehen haben) reduziert diese Race-Bedingung, indem Abonnement / Abmeldung wirksam wird, während es ausgelöst wird. Ich vermute jedoch, dass dies in den meisten Fällen, in denen das normale Verhalten nicht gut genug ist, auch nicht der Fall ist.
Jon Skeet

52

Ich sehe viele Leute, die sich der Erweiterungsmethode nähern ...

public static class Extensions   
{   
  public static void Raise<T>(this EventHandler<T> handler, 
    object sender, T args) where T : EventArgs   
  {   
    if (handler != null) handler(sender, args);   
  }   
}

Das gibt Ihnen eine schönere Syntax, um das Ereignis auszulösen ...

MyEvent.Raise( this, new MyEventArgs() );

Außerdem wird die lokale Kopie entfernt, da sie zum Zeitpunkt des Methodenaufrufs erfasst wird.


9
Ich mag die Syntax, aber lassen Sie uns klar sein ... es löst nicht das Problem, dass ein veralteter Handler aufgerufen wird, selbst wenn er nicht registriert wurde. Dies löst nur das Null-Dereferenzierungsproblem. Obwohl mir die Syntax gefällt, frage ich mich, ob sie wirklich besser ist als: public event EventHandler <T> MyEvent = delete {}; ... MyEvent (dies, neue MyEventArgs ()); Dies ist auch eine sehr reibungsarme Lösung, die ich wegen ihrer Einfachheit mag.
Simon Gillbee

@ Simon Ich sehe verschiedene Leute, die verschiedene Dinge dazu sagen. Ich habe es getestet und was ich getan habe, zeigt mir, dass dies das Null-Handler-Problem behandelt. Selbst wenn sich die ursprüngliche Senke nach der Prüfung des Handlers! = Null vom Ereignis abmeldet, wird das Ereignis immer noch ausgelöst und es wird keine Ausnahme ausgelöst.
JP Alioto


1
+1. Ich habe diese Methode gerade selbst geschrieben, angefangen, über Thread-Sicherheit nachzudenken, einige Nachforschungen angestellt und bin auf diese Frage gestoßen.
Niels van der Rest

Wie kann dies von VB.NET aus aufgerufen werden? Oder ist 'RaiseEvent' bereits für Multithreading-Szenarien geeignet?

35

"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:

  • Kosten für den Aufbau des leeren Delegaten.
  • Kosten für den Aufbau einer Delegatenkette, die diese enthält.
  • Kosten für das Aufrufen des sinnlosen Delegaten bei jeder Auslösung des Ereignisses.

(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:

Executing 50000000 iterations . . .
OnNonThreadSafeEvent took:      432ms
OnClassicNullCheckedEvent took: 490ms
OnPreInitializedEvent took:     614ms <--
Subscribing an empty delegate to each event . . .
Executing 50000000 iterations . . .
OnNonThreadSafeEvent took:      674ms
OnClassicNullCheckedEvent took: 674ms
OnPreInitializedEvent took:     2041ms <--
Subscribing another empty delegate to each event . . .
Executing 50000000 iterations . . .
OnNonThreadSafeEvent took:      2011ms
OnClassicNullCheckedEvent took: 2061ms
OnPreInitializedEvent took:     2246ms <--
Done

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.)


8
Ich denke, Sie machen den entscheidenden Punkt in Ihrem Blog-Beitrag: Sie müssen sich keine Gedanken über die Auswirkungen auf die Leistung machen, bis es sich um einen Engpass handelt. Warum sollte der hässliche Weg der empfohlene sein? Wenn wir eine vorzeitige Optimierung anstelle von Klarheit wünschen, würden wir Assembler verwenden - meine Frage bleibt also bestehen, und ich denke, die wahrscheinliche Antwort ist, dass der Rat einfach vor anonymen Delegierten liegt und es lange dauert, bis die menschliche Kultur alte Ratschläge wie z in der berühmten "Schmorbratengeschichte".
Daniel Earwicker

13
Und Ihre Zahlen belegen dies sehr gut: Der Overhead beträgt nur zweieinhalb NANOSECONDS (!!!) pro ausgelöstem Ereignis (Pre-Init vs. Classic-Null). Dies wäre in fast Apps, in denen echte Arbeit zu erledigen ist, nicht erkennbar. Da jedoch die überwiegende Mehrheit der Ereignisnutzung in GUI-Frameworks erfolgt, müssen Sie dies mit den Kosten für das Neulackieren von Teilen eines Bildschirms in Winforms usw. vergleichen In der Flut echter CPU-Arbeit und des Wartens auf Ressourcen wird es noch unsichtbarer. Du bekommst sowieso +1 von mir für die harte Arbeit. :)
Daniel Earwicker

1
@ DanielEarwicker hat es richtig gesagt, Sie haben mich dazu bewegt, an das öffentliche Ereignis zu glauben. WrapperDoneHandler OnWrapperDone = (x, y) => {}; Modell.
Mickey Perlstein

1
In Fällen, in denen das Ereignis null, einen oder zwei Teilnehmer hat, kann es auch sinnvoll sein, ein Delegate.Combine/ Delegate.RemovePaar zeitlich festzulegen. Wenn einer wiederholt dieselbe Delegateninstanz hinzufügt und entfernt, sind die Kostenunterschiede zwischen den Fällen besonders ausgeprägt, da er Combinesich schnell in Sonderfällen verhält, wenn eines der Argumente ist null(nur das andere zurückgeben), und Removesehr schnell ist, wenn die beiden Argumente vorliegen gleich (nur null zurückgeben).
Supercat

12

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!

public event Action<thisClass, string> Some;
protected virtual void DoSomeEvent(string someValue)
{
  var e = Some; // avoid race condition here! 
  if(null != e) // avoid null condition here! 
     e(this, someValue);
}

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?


Einverstanden, sollte dies im Compiler besser unterstützt werden. Bis dahin habe ich einen PostSharp-Aspekt erstellt, der dies in einem Schritt nach dem Kompilieren ausführt . :)
Steven Jeuris

4
Das Blockieren von Thread-Abonnement- / Abmeldeanforderungen während des Wartens auf den Abschluss eines beliebigen externen Codes ist weitaus schlimmer als das Empfangen von Ereignissen durch Abonnenten nach dem Kündigen von Abonnements , insbesondere da das letztere "Problem" einfach gelöst werden kann, indem Ereignishandler einfach ein Flag überprüfen, um festzustellen, ob Sie sind immer noch daran interessiert, ihre Veranstaltung zu erhalten, aber Deadlocks, die sich aus dem früheren Design ergeben, können unlösbar sein.
Supercat

@ Supercat. Imo, der "weitaus schlechtere" Kommentar ist ziemlich anwendungsabhängig. Wer möchte nicht eine sehr strenge Verriegelung ohne zusätzliche Flags, wenn dies eine Option ist? Ein Deadlock sollte nur auftreten, wenn der Ereignishandhabungsthread auf einen weiteren Thread wartet (der abonniert / abbestellt), da Sperren vom selben Thread erneut eingegeben werden und ein Abonnieren / Abbestellen innerhalb des ursprünglichen Ereignishandlers nicht blockiert wird. Wenn als Teil eines Ereignishandlers ein Cross-Thread wartet, der Teil des Entwurfs wäre, würde ich es vorziehen, ihn zu überarbeiten. Ich komme aus einem serverseitigen App-Blickwinkel mit vorhersehbaren Mustern.
Crokusek

2
@crokusek: Die Analyse, die erforderlich ist, um zu beweisen, dass ein System frei von Deadlocks ist, ist einfach, wenn in einem gerichteten Diagramm keine Zyklen vorhanden wären, die jede Sperre mit allen Sperren verbinden, die möglicherweise benötigt werden, während sie gehalten wird [das Fehlen von Zyklen beweist das System Deadlock-frei]. Wenn Sie zulassen, dass beliebiger Code aufgerufen wird, während eine Sperre gehalten wird, wird im Diagramm "möglicherweise erforderlich" eine Kante von dieser Sperre zu jeder Sperre erstellt, die von beliebigem Code erfasst werden kann (nicht ganz jede Sperre im System, aber nicht weit davon entfernt) ). Die konsequente Existenz von Zyklen würde nicht bedeuten, dass ein Deadlock auftreten würde, aber ...
Supercat

1
... würde den Grad der Analyse, der erforderlich ist, um zu beweisen, dass dies nicht möglich ist, erheblich erhöhen.
Supercat

8

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.


5

Laut Jeffrey Richter im Buch CLR via C # ist die richtige Methode:

// Copy a reference to the delegate field now into a temporary field for thread safety
EventHandler<EventArgs> temp =
Interlocked.CompareExchange(ref NewMail, null, null);
// If any methods registered interest with our event, notify them
if (temp != null) temp(this, e);

Weil es eine Referenzkopie erzwingt. Weitere Informationen finden Sie in seinem Abschnitt "Veranstaltung" im Buch.


Vielleicht fehlt mir smth, aber Interlocked.CompareExchange löst eine NullReferenceException aus, wenn das erste Argument null ist, was genau das ist, was wir vermeiden wollen. msdn.microsoft.com/en-us/library/bb297966.aspx
Kniganapolke

1
Interlocked.CompareExchangewürde scheitern , wenn es irgendwie eine Null übergeben wurde ref, aber das ist nicht dasselbe wie ein übergeben wird refan einen Lagerort (zB NewMail) , die vorhanden ist, und die zunächst hält eine Nullreferenz.
Supercat

4

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.

private readonly object eventMutex = new object();

private event EventHandler _onEvent = null;

public event EventHandler OnEvent
{
  add
  {
    lock(eventMutex)
    {
      _onEvent += value;
    }
  }

  remove
  {
    lock(eventMutex)
    {
      _onEvent -= value;
    }
  }

}

private void HandleEvent(EventArgs args)
{
  lock(eventMutex)
  {
    if (_onEvent != null)
      _onEvent(args);
  }
}

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.


Eigentlich sehe ich, dass jemand anderes hier ein sehr ähnliches Muster verwendet: stackoverflow.com/questions/3668953/…
Ash

2

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


Ich bitte nicht um eine Lösung für dieses Problem. Ich frage mich, warum es weit verbreitete Ratschläge gibt, zusätzliche Code-Verwirrung um das Auslösen von Ereignissen zu sprühen, wenn eine Null-Ausnahme nur dann vermieden wird, wenn ein schwer zu erkennender Rennzustand vorliegt, der noch besteht.
Daniel Earwicker

1
Nun, das war mein Punkt. Sie kümmern sich nicht um die Rennbedingungen. Sie kümmern sich nur um die Nullreferenzausnahme. Ich werde das in meiner Antwort bearbeiten.
dss539

Und mein Punkt ist: Warum sollte es sinnvoll sein, sich um die Nullreferenzausnahme und nicht um die Rennbedingungen zu kümmern?
Daniel Earwicker

4
Ein ordnungsgemäß geschriebener Ereignishandler sollte darauf vorbereitet sein, die Tatsache zu behandeln, dass eine bestimmte Anforderung zum Auslösen eines Ereignisses, deren Verarbeitung sich mit einer Anforderung zum Hinzufügen oder Entfernen überschneiden kann, möglicherweise das Ereignis auslöst, das hinzugefügt oder entfernt wurde. Der Grund, warum sich Programmierer nicht um die Rennbedingungen kümmern, ist, dass es in richtig geschriebenem Code keine Rolle spielt, wer gewinnt .
Supercat

2
@ dss539: Während man ein Ereignis-Framework entwerfen könnte, das Abmeldeanforderungen blockiert, bis ausstehende Ereignisaufrufe abgeschlossen sind, würde ein solches Design es einem Ereignis (sogar so etwas wie einem UnloadEreignis) 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.
Supercat

1

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.


Ich gehe davon aus, dass Sie "die Verwendung eines leeren Delegaten anstelle von ..." meinen. Sie können bereits das tun, was Sie vorschlagen, indem ein Ereignis für den leeren Delegierten initialisiert wird. Der Test (MyEvent.GetInvocationList (). Length == 1) ist wahr, wenn nur der erste leere Delegat in der Liste enthalten ist. Es wäre immer noch nicht nötig, zuerst eine Kopie zu erstellen. Obwohl ich denke, dass der von Ihnen beschriebene Fall sowieso äußerst selten wäre.
Daniel Earwicker

Ich denke, wir bringen hier die Ideen von Delegierten und Veranstaltungen zusammen. Wenn ich ein Ereignis Foo in meiner Klasse habe, rufen externe Benutzer, wenn sie MyType.Foo + = / - = aufrufen, tatsächlich die Methoden add_Foo () und remove_Foo () auf. Wenn ich jedoch innerhalb der Klasse, in der es definiert ist, auf Foo verweise, verweise ich tatsächlich direkt auf den zugrunde liegenden Delegaten, nicht auf die Methoden add_Foo () und remove_Foo (). Und mit der Existenz von Typen wie EventHandlerList gibt es nichts, was vorschreibt, dass sich der Delegat und das Ereignis sogar am selben Ort befinden. Dies habe ich in meiner Antwort mit dem Absatz "Denken Sie daran" gemeint.
Levi

(Forts.) Ich gebe zu, dass dies ein verwirrendes Design ist, aber die Alternative könnte schlechter gewesen sein. Da Sie am Ende nur einen Delegaten haben - Sie können den zugrunde liegenden Delegaten direkt referenzieren, ihn aus einer Sammlung abrufen, ihn sofort instanziieren -, war es möglicherweise technisch unmöglich, etwas anderes als die "Prüfung auf" zu unterstützen null "Muster.
Levi

Da wir über das Auslösen des Ereignisses sprechen, kann ich nicht verstehen, warum die Addors zum Hinzufügen / Entfernen hier wichtig sind.
Daniel Earwicker

@ Levi: Ich mag die Art und Weise, wie C # mit Ereignissen umgeht, wirklich nicht. Wenn ich meine Druthers hätte, hätte der Delegierte einen anderen Namen als die Veranstaltung erhalten. Von außerhalb einer Klasse wären die einzigen zulässigen Operationen für einen Ereignisnamen +=und -=. Innerhalb der Klasse umfassen die zulässigen Operationen auch den Aufruf (mit integrierter Nullprüfung), das Testen gegen nulloder das Setzen auf null. Für alles andere müsste man den Delegaten verwenden, dessen Name der Ereignisname mit einem bestimmten Präfix oder Suffix wäre.
Superkatze

0

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.


0

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


Wenn Sie eine Instanz einer Klasse mit veränderlichem Status zuweisen (Felder, deren Werte geändert werden) und dann mehrere Threads gleichzeitig auf dieselbe Instanz zugreifen lassen, ohne Sperren zu verwenden, um diese Felder davor zu schützen, von zwei Threads gleichzeitig geändert zu werden, sind Sie es wahrscheinlich falsch machen. Wenn alle Ihre Threads ihre eigenen separaten Instanzen haben (nichts gemeinsam nutzen) oder alle Ihre Objekte unveränderlich sind (einmal zugewiesen, ändern sich die Werte ihrer Felder nie), sind Sie wahrscheinlich in Ordnung.
Daniel Earwicker

Mein allgemeiner Ansatz besteht darin, die Synchronisation dem Aufrufer zu überlassen, außer bei statischen Methoden. Wenn ich der Anrufer bin, synchronisiere ich auf dieser höheren Ebene. (Mit Ausnahme eines Objekts, dessen einziger Zweck natürlich der synchronisierte Zugriff ist. :))
Greg D

@GregD das hängt davon ab, wie komplex die Methode ist und welche Daten sie verwendet. Wenn es interne Mitglieder betrifft und Sie sich entscheiden, in einem Threaded / Tasked-Zustand zu laufen, werden Sie sehr verletzt
Mickey Perlstein

0

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.


[1] Es gibt keinen nützlichen Ereignishandler, der "zu einem bestimmten Zeitpunkt" auftritt. Alle Operationen haben eine Zeitspanne. Jeder einzelne Handler kann eine nicht triviale Folge von Schritten ausführen. Die Unterstützung einer Liste von Handlern ändert nichts.
Daniel Earwicker

[2] Ein Schloss zu halten, während ein Ereignis ausgelöst wird, ist totaler Wahnsinn. Es führt unweigerlich zum Stillstand. Die Quelle nimmt die Sperre A auf, löst ein Ereignis aus, die Senke nimmt die Sperre B heraus, jetzt werden zwei Sperren gehalten. Was ist, wenn eine Operation in einem anderen Thread dazu führt, dass die Sperren in umgekehrter Reihenfolge aufgehoben werden? Wie können solche tödlichen Kombinationen ausgeschlossen werden, wenn die Verantwortung für die Verriegelung auf separat entworfene / getestete Komponenten aufgeteilt wird (worauf es ankommt)?
Daniel Earwicker

[3] Keines dieser Probleme beeinträchtigt in irgendeiner Weise die allgegenwärtige Nützlichkeit normaler Multicast-Delegaten / -Ereignisse bei der Single-Threaded-Zusammensetzung von Komponenten, insbesondere in GUI-Frameworks. Dieser Anwendungsfall deckt die überwiegende Mehrheit der Verwendungszwecke von Ereignissen ab. Die Verwendung von Ereignissen im freien Thread ist von fragwürdigem Wert. Dies macht ihr Design oder ihre offensichtliche Nützlichkeit in den Kontexten, in denen sie sinnvoll sind, in keiner Weise ungültig.
Daniel Earwicker

[4] Threads + synchrone Ereignisse sind im Wesentlichen ein roter Hering. Asynchrone Kommunikation in der Warteschlange ist der richtige Weg.
Daniel Earwicker

[1] Ich bezog mich nicht auf die gemessene Zeit ... Ich sprach über atomare Operationen, die logischerweise sofort auftreten ... und damit meine ich, dass sich nichts anderes ändern kann, wenn dieselben Ressourcen verwendet werden, die sie verwenden, während das Ereignis stattfindet weil es mit einem Schloss serialisiert ist.
Triynko

0

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

public static event EventHandler<EventArgs> PreInitializedEvent = delegate { };  

public static void OnPreInitializedEvent(EventArgs e)  
{  
    // No check required - event will never be null because  
    // we have subscribed an empty anonymous delegate which  
    // can never be unsubscribed. (But causes some overhead.)  
    PreInitializedEvent(null, e);  
}  

0

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

  • Möglichkeit, einen Thread während einer Erhöhung zu unter- oder abzumelden (Racebedingung entfernt)
  • Operatorüberladungen für + = und - = auf Klassenebene.
  • Generischer vom Anrufer definierter Delegat

0

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.

/// <summary>
/// Thread safe event invoker
/// </summary>
public sealed class ThreadSafeEventInvoker
{
    /// <summary>
    /// Dictionary of delegates
    /// </summary>
    readonly ConcurrentDictionary<Delegate, DelegateHolder> delegates = new ConcurrentDictionary<Delegate, DelegateHolder>();

    /// <summary>
    /// List of delegates to be called, we need it because it is relatevely easy to implement a loop with list
    /// modification inside of it
    /// </summary>
    readonly LinkedList<DelegateHolder> delegatesList = new LinkedList<DelegateHolder>();

    /// <summary>
    /// locker for delegates list
    /// </summary>
    private readonly ReaderWriterLockSlim listLocker = new ReaderWriterLockSlim();

    /// <summary>
    /// Add delegate to list
    /// </summary>
    /// <param name="value"></param>
    public void Add(Delegate value)
    {
        var holder = new DelegateHolder(value);
        if (!delegates.TryAdd(value, holder)) return;

        listLocker.EnterWriteLock();
        delegatesList.AddLast(holder);
        listLocker.ExitWriteLock();
    }

    /// <summary>
    /// Remove delegate from list
    /// </summary>
    /// <param name="value"></param>
    public void Remove(Delegate value)
    {
        DelegateHolder holder;
        if (!delegates.TryRemove(value, out holder)) return;

        Monitor.Enter(holder);
        holder.IsDeleted = true;
        Monitor.Exit(holder);
    }

    /// <summary>
    /// Raise an event
    /// </summary>
    /// <param name="args"></param>
    public void Raise(params object[] args)
    {
        DelegateHolder holder = null;

        try
        {
            // get root element
            listLocker.EnterReadLock();
            var cursor = delegatesList.First;
            listLocker.ExitReadLock();

            while (cursor != null)
            {
                // get its value and a next node
                listLocker.EnterReadLock();
                holder = cursor.Value;
                var next = cursor.Next;
                listLocker.ExitReadLock();

                // lock holder and invoke if it is not removed
                Monitor.Enter(holder);
                if (!holder.IsDeleted)
                    holder.Action.DynamicInvoke(args);
                else if (!holder.IsDeletedFromList)
                {
                    listLocker.EnterWriteLock();
                    delegatesList.Remove(cursor);
                    holder.IsDeletedFromList = true;
                    listLocker.ExitWriteLock();
                }
                Monitor.Exit(holder);

                cursor = next;
            }
        }
        catch
        {
            // clean up
            if (listLocker.IsReadLockHeld)
                listLocker.ExitReadLock();
            if (listLocker.IsWriteLockHeld)
                listLocker.ExitWriteLock();
            if (holder != null && Monitor.IsEntered(holder))
                Monitor.Exit(holder);

            throw;
        }
    }

    /// <summary>
    /// helper class
    /// </summary>
    class DelegateHolder
    {
        /// <summary>
        /// delegate to call
        /// </summary>
        public Delegate Action { get; private set; }

        /// <summary>
        /// flag shows if this delegate removed from list of calls
        /// </summary>
        public bool IsDeleted { get; set; }

        /// <summary>
        /// flag shows if this instance was removed from all lists
        /// </summary>
        public bool IsDeletedFromList { get; set; }

        /// <summary>
        /// Constuctor
        /// </summary>
        /// <param name="d"></param>
        public DelegateHolder(Delegate d)
        {
            Action = d;
        }
    }
}

Und die Verwendung ist:

    private readonly ThreadSafeEventInvoker someEventWrapper = new ThreadSafeEventInvoker();
    public event Action SomeEvent
    {
        add { someEventWrapper.Add(value); }
        remove { someEventWrapper.Remove(value); }
    }

    public void RaiseSomeEvent()
    {
        someEventWrapper.Raise();
    }

Prüfung

Ich habe es folgendermaßen getestet. Ich habe einen Thread, der Objekte wie dieses erstellt und zerstört:

var objects = Enumerable.Range(0, 1000).Select(x => new Bar(foo)).ToList();
Thread.Sleep(10);
objects.ForEach(x => x.Dispose());

In einem BarKonstruktor (einem Listener-Objekt), den ich abonniereSomeEvent (der wie oben gezeigt implementiert ist) und melde in Dispose:

    public Bar(Foo foo)
    {
        this.foo = foo;
        foo.SomeEvent += Handler;
    }

    public void Handler()
    {
        if (disposed)
            Console.WriteLine("Handler is called after object was disposed!");
    }

    public void Dispose()
    {
        foo.SomeEvent -= Handler;
        disposed = true;
    }

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?


Sieht gut genug für mich aus. Obwohl ich denke, dass es (theoretisch) möglich sein könnte, dass disposed = truedies zuvor foo.SomeEvent -= Handlerin 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 wirklich try ... finallyfü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önnte try catch. Und Sie überprüfen nicht den Delegierten, der Add/ übergeben wurde Remove- es könnte sein null(Sie sollten sofort in Add/ werfen Remove).
Luaan
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.