erklärung - c# winforms update ui from thread




C#Ereignisse und Threadsicherheit (10)

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

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?


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.


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


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 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?


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.



I don't believe the question is constrained to the c# "event" type. Removing that restriction, why not re-invent the wheel a bit and do something along these lines?

Raise event thread safely - best practice

  • Ability to sub/unsubscribe from any thread while within a raise (race condition removed)
  • Operator overloads for += and -= at the class level.
  • Generic caller-defined delegate

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);  
}  




events