multithreading erklärung - C#Ereignisse und Threadsicherheit




invoke ui (13)

Please take a look here: http://www.danielfortunov.com/software/%24daniel_fortunovs_adventures_in_software_development/2009/04/23/net_event_invocation_thread_safety This is the correct solution and should always be used instead of all other workarounds.

“You can ensure that the internal invocation list always has at least one member by initializing it with a do-nothing anonymous method. Because no external party can have a reference to the anonymous method, no external party can remove the method, so the delegate will never be null” — Programming .NET Components, 2nd Edition, by 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);  
}  

AKTUALISIEREN

Ab C # 6 lautet die Antwort auf diese Frage:

SomeEvent?.Invoke(this, e);

Ich höre / höre häufig folgenden Rat:

Erstellen Sie immer eine Kopie eines Ereignisses, bevor Sie es auf null überprüfen und es null . Dadurch wird ein potenzielles Problem mit dem Threading beseitigt, bei dem das Ereignis an der Position zwischen Null und dem Ereignis, an dem Sie das Ereignis auslösen, gleich null wird.

// 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 : Nach dem Lesen von Optimierungen dachte ich, dass das Event-Member möglicherweise auch flüchtig sein könnte, aber Jon Skeet gibt in seiner Antwort an, dass die CLR die Kopie nicht optimiert.

Damit dieses Problem jedoch auftritt, muss ein anderer Thread in etwa 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 Sequenz 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 nachdem der Autor sich abgemeldet hat und sie sich gerade abgemeldet haben, um dies zu vermeiden. Was wirklich benötigt wird, ist eine benutzerdefinierte Event-Implementierung mit entsprechender Synchronisation in den add und remove Accessoren. Und zusätzlich gibt es das Problem möglicher Deadlocks, wenn eine Sperre gehalten wird, während ein Ereignis ausgelöst wird.

Ist das Cargo Cult Programming ? Es scheint so - viele Leute müssen diesen Schritt tun, um ihren Code vor mehreren Threads zu schützen, während es in Wirklichkeit scheint, dass Ereignisse viel mehr Sorgfalt erfordern, bevor sie als Teil eines Multi-Thread-Designs verwendet werden können . Folglich können Menschen, die diese zusätzliche Pflege nicht nehmen, diesen Rat auch ignorieren - es ist einfach kein Problem für single-threaded Programme, und in der Tat, angesichts der Abwesenheit von volatile in den meisten Online-Beispiel-Code, kann der Rat haben überhaupt keine Wirkung.

(Und ist es nicht viel einfacher, einfach den leeren delegate { } auf die Member-Deklaration zuzuweisen, so dass Sie nie nach null suchen müssen?)

Aktualisiert: Wenn es nicht klar war, habe ich die Absicht des Rates verstanden - unter allen Umständen eine Null-Referenz-Ausnahme zu vermeiden. Mein Punkt ist, dass diese spezielle NULL-Referenz-Ausnahme nur auftreten kann, wenn ein anderer Thread von dem Ereignis dekotiert, und der einzige Grund dafür ist sicherzustellen, dass keine weiteren Aufrufe über dieses Ereignis empfangen werden, was durch diese Technik eindeutig NICHT erreicht wird . Sie würden eine Race-Bedingung verbergen - es wäre besser, es zu enthüllen! Diese Null-Ausnahme hilft beim Erkennen eines Missbrauchs Ihrer Komponente. Wenn Sie möchten, dass Ihre Komponente vor Missbrauch geschützt ist, könnten Sie dem Beispiel von WPF folgen - speichern Sie die Thread-ID in Ihrem Konstruktor und werfen Sie dann eine Ausnahme, wenn ein anderer Thread versucht, direkt mit Ihrer Komponente zu interagieren. Oder implementieren Sie eine wirklich thread-sichere Komponente (keine leichte Aufgabe).

Ich behaupte also, dass das bloße Ausführen dieser Kopier- / Prüfsprache eine Cargokult-Programmierung ist, die Ihrem Code Unordnung und Rauschen hinzufügt. Um tatsächlich gegen andere Threads zu schützen, ist viel mehr Arbeit nötig.

Update als Antwort auf Eric Lipperts Blogposts:

Es gab also eine wichtige Sache, die ich an Event-Handlern vermisst hatte: "Event-Handler müssen robust sein, wenn sie aufgerufen werden, auch wenn das Event nicht abonniert wurde", und deshalb müssen wir uns nur um die Möglichkeit des Events kümmern Delegat ist null . Ist diese Anforderung für Ereignishandler überall dokumentiert?

Und so: "Es gibt andere Möglichkeiten, um dieses Problem zu lösen; zum Beispiel, den Handler zu initialisieren, um eine leere Aktion zu haben, die nie entfernt wird. Aber eine Nullkontrolle ist das Standardmuster."

Das einzige verbleibende Fragment meiner Frage ist, warum ist das "Standardmuster" explizit-null-check? Die Alternative, die den leeren Delegaten zuweist, benötigt nur = delegate {} , um der Ereignisdeklaration hinzugefügt zu werden, und dies beseitigt jene kleinen Stapel stinkender Zeremonien von jedem Ort, an dem das Ereignis ausgelöst wird. Es wäre einfach sicherzustellen, dass der leere Delegat billig zu instanziieren ist. Oder verpasse ich noch etwas?

Sicherlich muss es (wie Jon Skeet vorgeschlagen hat) nur .NET 1.x Ratschlag geben, der nicht ausgestorben ist, wie es 2005 hätte sein sollen?


Verbinden Sie alle Ihre Veranstaltungen im Bau und lassen Sie sie in Ruhe. Das Design der Delegate-Klasse kann möglicherweise keine andere Verwendung korrekt behandeln, wie ich im letzten Absatz dieses Posts erläutern werde.

Zuallererst ist es sinnlos, eine Ereignisbenachrichtigung abzufangen, wenn Ihre Event- Handler bereits eine synchronisierte Entscheidung treffen müssen, ob und wie auf die Benachrichtigung reagiert werden soll .

Alles, was mitgeteilt werden kann, sollte benachrichtigt werden. Wenn Ihre Ereignishandler die Benachrichtigungen ordnungsgemäß verarbeiten (dh sie haben Zugriff auf einen autorisierenden Anwendungsstatus und antworten nur, wenn dies angemessen ist), können Sie sie jederzeit benachrichtigen und darauf vertrauen, dass sie ordnungsgemäß reagieren.

Die einzige Zeit, zu der 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 dafür verantwortlich ist, das Ereignis zu erkennen und überhaupt erst hervorzurufen).

Ehrlich gesagt denke ich, dass die Delegiertenklasse nicht verlustbringend ist. Die Fusion / der Übergang zu einem MulticastDelegate war ein großer Fehler, weil es die (nützliche) Definition eines Ereignisses von etwas, das zu einem bestimmten Zeitpunkt passiert, zu etwas geändert hat, das über einen Zeitraum passiert. Eine solche Änderung erfordert einen Synchronisationsmechanismus, der sie logisch in einen einzelnen Augenblick zurückführen kann, aber dem MulticastDelegate fehlt ein solcher Mechanismus. Die Synchronisierung sollte die gesamte Zeitspanne oder den Zeitpunkt umfassen, an dem das Ereignis stattfindet, sodass, sobald eine Anwendung die synchronisierte Entscheidung getroffen hat, mit der Behandlung eines Ereignisses zu beginnen, diese vollständig (transaktional) verarbeitet wird. Mit der Blackbox, die die Hybridklasse MulticastDelegate / Delegate ist, ist dies nahezu unmöglich, also halten Sie sich an einen Single-Subscriber und / oder implementieren Sie Ihre eigene Art von MulticastDelegate mit einem Synchronisations-Handle, das herausgenommen werden kann, während die Handler-Kette ist benutzt / modifiziert werden . Ich empfehle das, weil die Alternative wäre, die Synchronisation / Transaktionsintegrität redundant in all Ihren Handlern zu implementieren, was lächerlich / unnötig komplex wäre.



"Warum wird das 'Standardmuster' explizit-null-geprüft?"

Ich vermute, der Grund dafür könnte sein, dass der Null-Check leistungsfähiger ist.

Wenn Sie bei der Erstellung immer einen leeren Delegaten für Ihre Ereignisse abonnieren, entstehen einige Gemeinkosten:

  • Kosten für den Aufbau des leeren Delegaten.
  • Kosten für den Aufbau einer Delegatenkette, um sie zu enthalten.
  • Kosten, um den unwichtigen Delegierten jedes Mal aufzurufen, wenn das Ereignis ausgelöst wird.

(Beachten Sie, dass UI-Steuerelemente häufig eine große Anzahl von Ereignissen aufweisen, von denen die meisten nie abonniert werden. Das Erstellen eines Dummy-Abonnenten für jedes Ereignis und das anschließende Aufrufen des Ereignisses würde wahrscheinlich zu einem erheblichen Leistungseinbruch führen.)

Ich habe einige oberflächliche Leistungstests durchgeführt, um die Auswirkungen des Subscribe-Empty-Delegate-Ansatzes zu sehen, und 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 das Ereignis, das mit einem leeren Delegaten vorinitialisiert wurde, deutlich langsamer ist (über 50 Millionen Iterationen ...). Dies gilt für den Fall von null oder einem Abonnenten (häufig für UI-Steuerelemente, wo Ereignisse reichlich vorhanden sind).

Weitere Informationen und den Quellcode finden Sie in diesem Blogpost zur .NET-Ereignisaufruf-Thread-Sicherheit , den ich nur einen Tag vor der Beantwortung dieser Frage veröffentlicht habe (!)

(Mein Testaufbau könnte fehlerhaft sein, also zögern Sie nicht, den Quellcode herunterzuladen und ihn selbst zu überprüfen. Jedes Feedback wird sehr geschätzt.)


Bei dieser Übung geht es nicht darum, eine bestimmte Reihenfolge von Vorgängen zu erzwingen. Es geht darum, eine Null-Referenz-Ausnahme zu vermeiden.

Die Gründe hinter Menschen, die sich um die Null-Referenz-Ausnahme und nicht um die Rassenkondition kümmern, würden eine tiefgehende psychologische Forschung erfordern. Ich denke, es hat etwas mit der Tatsache zu tun, dass es viel einfacher ist, das Nullreferenzproblem zu beheben. Sobald dies behoben ist, hängen sie ein großes "Mission Accomplished" Banner auf ihren Code und entpacken ihren Fluganzug.

Hinweis: Zur Behebung der Racebedingung wird wahrscheinlich eine synchrone Flag-Spur verwendet, ob der Handler ausgeführt werden soll


Ich habe diese Lektüre wirklich genossen - nicht! Obwohl ich es brauche, um mit der C # -Funktion namens Events zu arbeiten!

Warum nicht das im Compiler beheben? Ich weiß, dass es MS-Leute gibt, die diese Posts lesen, also bitte flammt das nicht!

1 - das Null-Problem ) Warum sollten Ereignisse nicht erst "leer" anstatt "null" werden? Wie viele Codezeilen würden für den Null-Check gespeichert oder müssten ein = delegate {} auf die Deklaration gesetzt werden? Lassen Sie den Compiler den leeren Fall behandeln, IE nichts tun! Wenn alles für den Ersteller der Veranstaltung wichtig ist, können sie nach "Leer" suchen und alles tun, was sie interessiert! Ansonsten sind alle Null-Checks / Delegate-Adds Hacks um das Problem herum!

Ehrlich gesagt habe ich es satt, dass ich das bei jedem Event machen muss - aka 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 Race-Condition-Problem ) Ich lese Erics Blogpost, ich stimme zu, dass der H (Handler) damit umgehen sollte, wenn er sich selbst dereferenziert, aber kann das Event nicht unveränderlich / Thread-sicher gemacht werden? IE, setzen Sie ein Sperrkennzeichen bei seiner Erstellung, so dass jedes Mal, wenn es aufgerufen wird, alle Subskriptionen und Un-Subskriptionen, während es ausgeführt wird, sperren?

Fazit ,

Sollen moderne Sprachen solche Probleme für uns nicht lösen?


Für Single-Thread-Anwendungen, Sie sind richtig, das ist 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 durchführt. In diesem Fall müssen Sie sich auf das Schlimmste vorbereiten.

Die Verwendung des leeren Delegaten löst zwar das Problem, verursacht jedoch bei jedem Aufruf des Ereignisses einen Leistungseinbruch und könnte möglicherweise Auswirkungen auf die GC haben.

Sie haben Recht, dass der Konsument sich abmelden muss, um dies zu tun, aber wenn er die temporäre Kopie hinter sich gelassen hat, dann überlegen Sie sich die Nachricht, die bereits unterwegs ist.

Wenn Sie die temporäre Variable nicht verwenden und nicht den leeren Delegaten verwenden und sich jemand abmeldet, erhalten Sie eine Nullreferenzausnahme, die fatal ist. Daher denke ich, dass sich die Kosten gelohnt haben.


Ich sehe viele Leute, die auf die Erweiterungsmethode zugehen.

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 dir eine schönere Syntax, um das Ereignis zu erhöhen ...

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

Entfernt außerdem die lokale Kopie, da sie zum Zeitpunkt des Methodenaufrufs erfasst wird.


Also bin ich ein wenig zu spät zur Party hier. :)

Beziehen Sie sich für dieses Szenario auf die Verwendung von Null anstelle des Nullobjektmusters, um Ereignisse ohne Abonnenten darzustellen. Sie müssen ein Ereignis aufrufen, aber das Konstruieren des Objekts (EventArgs) ist nicht trivial und im allgemeinen Fall hat Ihr Ereignis keine Abonnenten. Es wäre von Vorteil für Sie, wenn Sie Ihren Code optimieren könnten, um zu überprüfen, ob Sie überhaupt Abonnenten hatten, bevor Sie die Verarbeitung der Argumente zum Konstruieren der Argumente und das Aufrufen des Ereignisses übernehmen.

In diesem Sinne ist eine Lösung zu sagen "Nun, null Abonnenten wird durch null dargestellt." Führen Sie dann einfach den Null-Check durch, bevor Sie Ihre teure Operation durchführen. Ich nehme an, eine andere Möglichkeit wäre eine Count-Eigenschaft auf dem Delegate-Typ zu haben, sodass Sie die teure Operation nur ausführen würden, wenn myDelegate.Count> 0 ist. Die Verwendung einer Count-Eigenschaft ist ein nettes Muster, das das ursprüngliche Problem löst Erlaubt die Optimierung und hat auch die nette Eigenschaft, aufgerufen zu werden, ohne eine NullReferenceException zu verursachen.

Beachten Sie jedoch, dass es sich bei den Delegierten um Referenztypen handelt, da sie null sein dürfen. Vielleicht gab es einfach keine gute Möglichkeit, diese Tatsache unter den Deckblättern zu verstecken und nur das Null-Objektmuster für Ereignisse zu unterstützen, so dass die Alternative Entwickler gezwungen haben könnte, sowohl nach Null- als auch nach Null-Teilnehmern zu suchen. Das wäre noch hässlicher als die jetzige Situation.

Hinweis: Dies ist reine Spekulation. Ich bin nicht mit den .NET-Sprachen oder CLR beteiligt.


Ich habe dieses Entwurfsmuster verwendet, um sicherzustellen, dass Ereignishandler nicht ausgeführt werden, nachdem sie sich abgemeldet haben. Es funktioniert bisher ziemlich gut, obwohl ich noch kein Performance-Profiling versucht 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 derzeit 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 gesendet wurde.


Laut Jeffrey Richter im Buch CLR via C # ist die korrekte 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 im Abschnitt "Ereignis" im Buch.


Der JIT darf die Optimierung, über die Sie im ersten Teil sprechen, aufgrund der Bedingung nicht ausführen. Ich weiß, dass das vor einer Weile als Gespenst angesprochen wurde, aber es ist nicht gültig. (Ich habe es vor einer Weile entweder mit Joe Duffy oder Vance Morrison überprüft; ich kann mich nicht erinnern, welches.)

Ohne den flüchtigen Modifikator ist es möglich, dass die lokale Kopie veraltet ist, aber das ist alles. Es wird keine NullReferenceException .

Und ja, es gibt sicherlich eine Race Condition - aber es wird immer sein. Angenommen, wir ändern den Code einfach zu:

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. (Delegierte sind unveränderlich.) Soweit ich sehen kann, ist dies unvermeidlich.

Die Verwendung eines leeren Delegierten vermeidet zwar die Nichtigkeitsprüfung, behebt jedoch nicht die Wettlaufbedingung. Es garantiert auch nicht, dass Sie immer den letzten Wert der Variablen "sehen".


System.String ist ein Alias ​​für System.String . Sie sind in C# .

Es gibt eine Debatte darüber, ob Sie die System.Int32 ( System.Int32 , System.String usw.) oder die C# aliases ( int , string usw.) verwenden sollten. Ich persönlich glaube, Sie sollten die C# aliases , aber das ist nur meine persönliche Präferenz.





c# multithreading events