Eventi C # e sicurezza dei thread


237

AGGIORNARE

A partire da C # 6, la risposta a questa domanda è:

SomeEvent?.Invoke(this, e);

Spesso ascolto / leggo i seguenti consigli:

Fai sempre una copia di un evento prima di controllarlo nulle attivarlo. Ciò eliminerà un potenziale problema con il threading in cui l'evento diventa nullnella posizione giusta tra dove si verifica la presenza di null e dove si genera l'evento:

// Copy the event delegate before checking/calling
EventHandler copy = TheEvent;

if (copy != null)
    copy(this, EventArgs.Empty); // Call any handlers on the copied list

Aggiornato : dalla lettura delle ottimizzazioni ho pensato che ciò potrebbe richiedere la volatilità del membro dell'evento, ma Jon Skeet afferma nella sua risposta che il CLR non ottimizza la copia.

Ma nel frattempo, per far sì che questo problema si verifichi, un altro thread deve aver fatto qualcosa del genere:

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

La sequenza effettiva potrebbe essere questa miscela:

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

Il punto è che OnTheEventcorre dopo che l'autore ha annullato l'iscrizione, eppure ha appena annullato l'iscrizione in modo specifico per evitare che ciò accada. Sicuramente ciò che è veramente necessario è un'implementazione di eventi personalizzati con un'adeguata sincronizzazione negli accessor adde remove. Inoltre, esiste il problema di possibili deadlock se si tiene un blocco mentre viene generato un evento.

Quindi è questa programmazione Cult Cargo ? Sembra in questo modo: molte persone devono fare questo passo per proteggere il loro codice da più thread, quando in realtà mi sembra che gli eventi richiedano molta più attenzione di questo prima che possano essere utilizzati come parte di un design multi-thread . Di conseguenza, le persone che non stanno prestando ulteriore attenzione potrebbero anche ignorare questo consiglio: semplicemente non è un problema per i programmi a thread singolo e, in effetti, data l'assenza della volatilemaggior parte del codice di esempio online, il consiglio potrebbe non avere effetto a tutti.

(E non è molto più semplice assegnare il vuoto delegate { }sulla dichiarazione del membro in modo da non dover mai cercare nullin primo luogo?)

aggiornato:Nel caso in cui non fosse chiaro, ho colto l'intenzione del consiglio: evitare un'eccezione di riferimento nulla in ogni circostanza. Il mio punto è che questa particolare eccezione di riferimento null può verificarsi solo se un altro thread sta cancellando dall'evento e l'unico motivo per farlo è quello di garantire che non vengano ricevute ulteriori chiamate tramite quell'evento, che NON è chiaramente raggiunto da questa tecnica . Nasconderesti una condizione di razza - sarebbe meglio rivelarla! Tale eccezione nulla aiuta a rilevare un abuso del componente. Se vuoi che il tuo componente sia protetto dagli abusi, potresti seguire l'esempio di WPF: memorizzare l'ID del thread nel costruttore e quindi generare un'eccezione se un altro thread tenta di interagire direttamente con il componente. Oppure implementa un componente veramente thread-safe (non un compito facile).

Quindi sostengo che semplicemente fare questo idioma di copia / controllo sia una programmazione di culto del carico, aggiungendo confusione e rumore al tuo codice. Per proteggere effettivamente contro altri thread richiede molto più lavoro.

Aggiornamento in risposta ai post del blog di Eric Lippert:

Quindi c'è una cosa importante che mi mancava per i gestori di eventi: "I gestori di eventi devono essere robusti di fronte al fatto di essere chiamati anche dopo che l'evento è stato cancellato", e ovviamente quindi dobbiamo solo preoccuparci della possibilità dell'evento essere delegato null. Tale requisito sui gestori di eventi è documentato ovunque?

E così: "Esistono altri modi per risolvere questo problema; ad esempio, inizializzare il gestore in modo che un'azione vuota non venga mai rimossa. Ma fare un controllo nullo è il modello standard".

Quindi l'unico frammento rimasto della mia domanda è, perché è esplicito-null-check il "modello standard"? L'alternativa, assegnando il delegato vuoto, richiede solo = delegate {}di essere aggiunta alla dichiarazione dell'evento, e questo elimina quelle piccole pile di puzzolenti cerimonie da ogni luogo in cui l'evento viene generato. Sarebbe facile assicurarsi che il delegato vuoto sia economico da istanziare. O mi manca ancora qualcosa?

Sicuramente deve essere che (come suggerì Jon Skeet) questo è solo un consiglio .NET 1.x che non si è spento, come avrebbe dovuto fare nel 2005?


4
Questa domanda è emersa in una discussione interna qualche tempo fa; Ho intenzione di blog questo da qualche tempo ormai. Il mio post sull'argomento è qui: Eventi e gare
Eric Lippert,

3
Stephen Cleary ha un articolo di CodeProject che esamina questo problema e conclude uno scopo generale, la soluzione "thread-safe" non esiste. Fondamentalmente spetta al chiamante dell'evento assicurarsi che il delegato non sia nullo, e al gestore dell'evento essere in grado di gestire l'invocazione dopo che è stata annullata l'iscrizione.
rkagerer,

3
@rkagerer: in realtà il secondo problema a volte deve essere gestito dal gestore eventi anche se i thread non sono coinvolti. Può accadere se un gestore di eventi dice a un altro gestore di annullare l'iscrizione all'evento attualmente in fase di gestione, ma quel secondo abbonato riceverà comunque l'evento (poiché è stato annullato l'iscrizione mentre era in corso la gestione).
Daniel Earwicker,

3
Aggiungere un abbonamento a un evento con zero abbonati, rimuovere l'unico abbonamento per l'evento, invocare un evento con zero abbonati e invocare un evento con esattamente un abbonato, sono tutte operazioni molto più veloci rispetto all'aggiunta / rimozione / invocazione di scenari che coinvolgono altri numeri di iscritti. L'aggiunta di un delegato fittizio rallenta il caso comune. Il vero problema con C # è che i suoi creatori hanno deciso di EventName(arguments)invocare il delegato dell'evento incondizionatamente, piuttosto che farlo invocare il delegato solo se non nullo (non fare nulla se nullo).
supercat,

Risposte:


100

Alla JIT non è consentito eseguire l'ottimizzazione di cui stai parlando nella prima parte, a causa della condizione. So che questo è stato sollevato come uno spettro qualche tempo fa, ma non è valido. (L'ho verificato con Joe Duffy o Vance Morrison qualche tempo fa; non ricordo quale.)

Senza il modificatore volatile è possibile che la copia locale presa non sia aggiornata, ma questo è tutto. Non causerà a NullReferenceException.

E sì, c'è sicuramente una condizione di gara - ma ci sarà sempre. Supponiamo di cambiare il codice in:

TheEvent(this, EventArgs.Empty);

Supponiamo ora che l'elenco di invocazioni per quel delegato abbia 1000 voci. È perfettamente possibile che l'azione all'inizio dell'elenco sia stata eseguita prima che un altro thread annulli la sottoscrizione di un gestore vicino alla fine dell'elenco. Tuttavia, quel gestore verrà comunque eseguito perché sarà un nuovo elenco. (I delegati sono immutabili.) Per quanto posso vedere, questo è inevitabile.

L'uso di un delegato vuoto evita certamente il controllo di nullità, ma non corregge le condizioni di gara. Inoltre, non garantisce che "vedi" sempre il valore più recente della variabile.


4
"Programmazione concorrente su Windows" di Joe Duffy copre l'ottimizzazione JIT e l'aspetto del modello di memoria della domanda; vedi code.logos.com/blog/2008/11/events_and_threads_part_4.html
Bradley Grainger

2
Ho accettato questo in base al commento sul fatto che il consiglio "standard" era pre-C # 2 e non sento nessuno che lo contraddica. A meno che non sia davvero costoso creare un'istanza dei tuoi argomenti dell'evento, basta mettere '= delegate {}' alla fine della dichiarazione dell'evento e quindi chiamare i tuoi eventi direttamente come se fossero metodi; non assegnare mai nulla a loro. (Le altre cose che ho introdotto per garantire che un gestore non venga chiamato dopo il delisting, tutto ciò era irrilevante ed è impossibile garantire, anche per il codice a thread singolo, ad esempio se il gestore 1 chiede al gestore 2 di cancellare, il gestore 2 verrà comunque chiamato successivo)
Daniel Earwicker,

2
L'unico problema (come sempre) sono le strutture, in cui non è possibile assicurarsi che vengano istanziate con qualcosa di diverso dai valori null nei loro membri. Ma le strutture fanno schifo.
Daniel Earwicker,

1
Informazioni su delegato vuoto, vedere anche questa domanda: stackoverflow.com/questions/170907/… .
Vladimir,

2
@Tony: c'è ancora fondamentalmente una condizione di competizione tra qualcosa di abbonamento / annullamento dell'iscrizione e l'esecuzione del delegato. Il tuo codice (dopo averlo appena sfogliato brevemente) riduce tale condizione di competizione consentendo all'abbonamento / annullamento dell'iscrizione di avere effetto mentre viene sollevato, ma sospetto che nella maggior parte dei casi in cui il comportamento normale non è abbastanza buono, questo non è neanche.
Jon Skeet,

52

Vedo molte persone andare verso il metodo di estensione per fare questo ...

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

Questo ti dà una sintassi migliore per aumentare l'evento ...

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

Inoltre elimina la copia locale poiché viene acquisita al momento della chiamata del metodo.


9
Mi piace la sintassi, ma sia chiaro ... non risolve il problema con un gestore stantio che viene invocato anche dopo che non è stato registrato. Questo risolve solo il problema di dereferenza nulla. Mentre mi piace la sintassi, mi chiedo se sia davvero meglio di: evento pubblico EventHandler <T> MyEvent = delete {}; ... MyEvent (this, new MyEventArgs ()); Questa è anche una soluzione a basso attrito che mi piace per la sua semplicità.
Simon Gillbee,

@Simon Vedo diverse persone dire cose diverse su questo. L'ho provato e ciò che ho fatto mi indica che questo gestisce il problema del gestore null. Anche se il sink originale annulla la registrazione dall'evento dopo il gestore! = Controllo null, l'evento viene comunque generato e non viene generata alcuna eccezione.
JP Alioto,


1
+1. Ho appena scritto questo metodo da solo, iniziando a pensare alla sicurezza del thread, ho fatto qualche ricerca e sono incappato in questa domanda.
Niels van der Rest

Come può essere chiamato da VB.NET? O 'RaiseEvent' soddisfa già scenari multi-threading?

35

"Perché è esplicito-null-check il 'modello standard'?"

Ho il sospetto che la ragione potrebbe essere che il controllo null è più performante.

Se sottoscrivi sempre un delegato vuoto ai tuoi eventi quando vengono creati, ci saranno alcune spese generali:

  • Costo di costruzione del delegato vuoto.
  • Costo di costruzione di una catena di delegati per contenerla.
  • Costo di invocazione del delegato inutile ogni volta che viene generato l'evento.

(Si noti che i controlli dell'interfaccia utente hanno spesso un gran numero di eventi, molti dei quali non sono mai abbonati. Dover creare un utente fittizio per ciascun evento e quindi invocarlo sarebbe probabilmente un notevole impatto sulle prestazioni.)

Ho fatto alcuni test delle prestazioni in corsivo per vedere l'impatto dell'approccio iscriviti-vuoto-delegato, ed ecco i miei risultati:

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

Si noti che nel caso di zero o uno degli abbonati (comune per i controlli dell'interfaccia utente, in cui gli eventi sono abbondanti), l'evento preinizializzato con un delegato vuoto è notevolmente più lento (oltre 50 milioni di iterazioni ...)

Per ulteriori informazioni e codice sorgente, visitare questo post sul blog sulla sicurezza del thread di invocazione di eventi .NET che ho pubblicato il giorno prima della domanda (!)

(La configurazione del mio test potrebbe essere difettosa, quindi non esitare a scaricare il codice sorgente e controllarlo da solo. Qualsiasi feedback è molto apprezzato.)


8
Penso che tu abbia sottolineato il punto chiave nel tuo post sul blog: non è necessario preoccuparsi delle conseguenze sulle prestazioni fino a quando non si verifica un collo di bottiglia. Perché lasciare che la brutta via sia quella raccomandata? Se volessimo l'ottimizzazione prematura anziché la chiarezza, utilizzeremmo l'assemblatore - quindi la mia domanda rimane, e penso che la risposta probabile sia che il consiglio precede semplicemente i delegati anonimi e ci vuole molto tempo perché la cultura umana cambi i vecchi consigli, come nella famosa "storia dell'arrosto in padella".
Daniel Earwicker

13
E le tue cifre dimostrano molto bene il punto: il sovraccarico si riduce a soli due NANOSECONDI (!!!) per evento generato (pre-init vs. classic-null). Questo non sarebbe rilevabile in quasi app con lavoro reale da svolgere, ma dato che la stragrande maggioranza dell'utilizzo degli eventi è nei framework GUI, dovresti confrontarlo con il costo di riverniciare parti di uno schermo in Winforms, ecc., Quindi diventa ancora più invisibile nel diluvio del vero lavoro della CPU e in attesa di risorse. Ricevi +1 da me per il duro lavoro, comunque. :)
Daniel Earwicker,

1
@DanielEarwicker ha detto bene, mi hai spinto a credere nell'evento pubblico WrapperDoneHandler OnWrapperDone = (x, y) => {}; modello.
Mickey Perlstein,

1
Potrebbe anche essere utile programmare una coppia Delegate.Combine/ Delegate.Removenel caso in cui l'evento abbia zero, uno o due iscritti; se uno aggiunge e rimuove ripetutamente la stessa istanza del delegato, le differenze di costo tra i casi saranno particolarmente pronunciate poiché Combineha un comportamento rapido nei casi speciali quando uno degli argomenti è null(basta restituire l'altro) ed Removeè molto veloce quando i due argomenti sono uguale (restituisce solo null).
supercat,

12

Mi è davvero piaciuta questa lettura - no! Anche se ne ho bisogno per funzionare con la funzione C # chiamata event!

Perché non risolvere questo problema nel compilatore? So che ci sono persone con SM che leggono questi post, quindi per favore non dare fuoco a questo!

1 - il problema Null ) Perché non fare in modo che gli eventi siano .Empty anziché null in primo luogo? Quante righe di codice verrebbero salvate per un controllo nullo o se dovessero essere incollate = delegate {}sulla dichiarazione? Lascia che il compilatore gestisca il caso Vuoto, IE non fa nulla! Se tutto ciò che conta per il creatore dell'evento, possono controllare .Svuoto e fare tutto ciò che gli interessa! In caso contrario, tutti i controlli null / aggiunte di delegati sono trucchi per risolvere il problema!

Sinceramente sono stanco di doverlo fare con ogni evento, anche noto come codice boilerplate!

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 - il problema delle condizioni di gara ) Ho letto il post sul blog di Eric, sono d'accordo che l'H (gestore) dovrebbe gestire quando si dereferenzia se stesso, ma l'evento non può essere reso immutabile / thread sicuro? IE, imposta un flag di blocco sulla sua creazione, in modo che ogni volta che viene chiamato, blocca tutte le iscrizioni e le cancellazioni durante l'esecuzione?

Conclusione ,

Le lingue moderne non dovrebbero risolvere problemi come questi per noi?


D'accordo, ci dovrebbe essere un migliore supporto per questo nel compilatore. Fino ad allora, ho creato un aspetto PostSharp che lo fa in un passaggio post-compilazione . :)
Steven Jeuris il

4
Ansante di sottoscrizione filo / richieste di ritiro bloccare in attesa di codice esterno arbitrario completo è di gran lunga peggiore che avere abbonati ricevono eventi successivi abbonamenti vengono annullati, soprattutto dal momento che quest'ultima "problema" può essere risolto facilmente semplicemente avendo gestori di eventi controllare una bandiera per vedere se sono ancora interessati a ricevere il loro evento, ma i deadlock derivanti dal precedente progetto potrebbero essere intrattabili.
supercat

@supercat. Imo, il commento "molto peggio" dipende dall'applicazione. Chi non vorrebbe un bloccaggio molto rigoroso senza bandiere extra quando è un'opzione? Un deadlock dovrebbe verificarsi solo se il thread di gestione degli eventi è in attesa di un altro thread (che sta eseguendo la sottoscrizione / annullamento dell'iscrizione) poiché i blocchi sono rientranti nello stesso thread e un abbonamento / annullamento della sottoscrizione all'interno del gestore eventi originale non verrebbe bloccato. Se c'è un thread incrociato in attesa come parte di un gestore eventi che sarebbe parte del progetto, preferirei rielaborare. Vengo dal punto di vista dell'app sul lato server con schemi prevedibili.
crokusek,

2
@crokusek: L'analisi richiesta per dimostrare che un sistema è privo di deadlock è semplice se non ci fossero cicli in un grafico diretto che collega ciascun blocco a tutti i blocchi che potrebbero essere necessari mentre è tenuto [la mancanza di cicli prova il sistema -stallo libero]. Consentire il richiamo di codice arbitrario mentre si tiene premuto un blocco creerà un limite nel grafico "necessario" da quel blocco a qualsiasi blocco che il codice arbitrario potrebbe acquisire (non tutti i blocchi nel sistema, ma non lontano da esso ). La conseguente esistenza di cicli non implicherebbe che si verificherebbe un deadlock, ma ...
supercat

1
... aumenterebbe notevolmente il livello di analisi necessario per dimostrare che non è possibile.
supercat,

8

Con C # 6 e versioni successive, il codice potrebbe essere semplificato utilizzando un nuovo ?.operatore come in:

TheEvent?.Invoke(this, EventArgs.Empty);

Ecco la documentazione MSDN.


5

Secondo Jeffrey Richter nel libro CLR via C # , il metodo corretto è:

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

Perché forza una copia di riferimento. Per ulteriori informazioni, consultare la sezione Eventi nel libro.


Forse mi manca smth, ma Interlocked.CompareExchange genera NullReferenceException se il suo primo argomento è null, che è esattamente ciò che vogliamo evitare. msdn.microsoft.com/en-us/library/bb297966.aspx
Kniganapolke

1
Interlocked.CompareExchangefallirebbe se venisse in qualche modo passato un null ref, ma non è la stessa cosa che essere passato refa una posizione di archiviazione (es. NewMail) che esiste e che inizialmente contiene un riferimento null.
supercat,

4

Ho usato questo modello di progettazione per assicurarmi che i gestori di eventi non vengano eseguiti dopo che sono stati cancellati. Finora funziona abbastanza bene, anche se non ho provato alcun profilo di performance.

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

Attualmente lavoro principalmente con Mono per Android e ad Android non sembra piacere quando tenti di aggiornare una vista dopo che la sua attività è stata inviata in background.


In realtà, vedo che qualcun altro sta usando un modello molto simile qui: stackoverflow.com/questions/3668953/…
Ash

2

Questa pratica non riguarda l'applicazione di un certo ordine di operazioni. In realtà si tratta di evitare un'eccezione di riferimento null.

Il ragionamento alla base delle persone che si preoccupano dell'eccezione di riferimento nulla e non delle condizioni della razza richiederebbe una profonda ricerca psicologica. Penso che abbia qualcosa a che fare con il fatto che risolvere il problema del riferimento null è molto più semplice. Una volta risolto il problema, appendono sul loro codice un grande striscione "Compiuto missione" e decomprimono la tuta di volo.

Nota: la correzione delle condizioni di gara implica probabilmente l'utilizzo di una traccia di bandiera sincrona se il gestore deve eseguire


Non sto chiedendo una soluzione a questo problema. Mi chiedo perché ci siano consigli diffusi per spruzzare un ulteriore pasticcio di codice attorno all'attivazione di eventi, quando si evita un'eccezione nulla quando c'è una condizione di gara difficile da rilevare, che esisterà ancora.
Daniel Earwicker,

1
Bene, quello era il mio punto. A loro non importa delle condizioni di gara. Si preoccupano solo dell'eccezione di riferimento null. Lo modificherò nella mia risposta.
dss539,

E il mio punto è: perché avrebbe senso preoccuparsi dell'eccezione di riferimento null e non preoccuparsi delle condizioni di gara?
Daniel Earwicker,

4
Un gestore di eventi scritto correttamente deve essere preparato per gestire il fatto che qualsiasi particolare richiesta di generare un evento la cui elaborazione potrebbe sovrapporsi a una richiesta di aggiunta o rimozione, può o meno finire per generare l'evento che è stato aggiunto o rimosso. Il motivo per cui i programmatori non si preoccupano delle condizioni di gara è che nel codice scritto correttamente non importa chi vince .
Supercat,

2
@ dss539: Mentre si potrebbe progettare un framework di eventi che bloccherebbe le richieste di annullamento dell'iscrizione fino al termine delle invocazioni di eventi in sospeso, tale design renderebbe impossibile per qualsiasi evento (anche qualcosa come un Unloadevento) annullare in modo sicuro le sottoscrizioni di un oggetto ad altri eventi. Cattiva. Meglio dire semplicemente che le richieste di annullamento dell'iscrizione causeranno la cancellazione degli eventi "eventualmente" e che gli abbonati agli eventi dovrebbero verificare quando vengono invocati se c'è qualcosa di utile da fare per loro.
supercat,

1

Quindi sono un po 'in ritardo alla festa qui. :)

Per quanto riguarda l'uso di null anziché il modello di oggetti null per rappresentare eventi senza abbonati, prendere in considerazione questo scenario. Devi invocare un evento, ma costruire l'oggetto (EventArgs) non è banale e, nel caso comune, il tuo evento non ha abbonati. Sarebbe utile se potessi ottimizzare il tuo codice per verificare se disponevi di abbonati prima di impegnarti a elaborare gli argomenti e a invocare l'evento.

Con questo in mente, una soluzione è dire "beh, zero abbonati è rappresentato da null". Quindi eseguire semplicemente il controllo null prima di eseguire un'operazione costosa. Suppongo che un altro modo di fare ciò sarebbe stato avere una proprietà Count sul tipo Delegato, quindi eseguiresti l'operazione costosa solo se myDelegate.Count> 0. L'uso di una proprietà Count è un modello piuttosto piacevole che risolve il problema originale di consentire l'ottimizzazione, e ha anche la bella proprietà di poter essere invocato senza causare una NullReferenceException.

Tieni presente, tuttavia, che, poiché i delegati sono tipi di riferimento, possono essere nulli. Forse non c'era semplicemente un buon modo per nascondere questo fatto sotto le coperte e supportare solo il modello di oggetti null per gli eventi, quindi l'alternativa potrebbe aver costretto gli sviluppatori a controllare sia gli abbonati null che quelli zero. Sarebbe anche più brutto della situazione attuale.

Nota: questa è pura speculazione. Non sono coinvolto con i linguaggi .NET o CLR.


Suppongo che tu intenda "l'uso del delegato vuoto anziché ..." Puoi già fare ciò che suggerisci, con un evento inizializzato al delegato vuoto. Il test (MyEvent.GetInvocationList (). Lunghezza == 1) sarà vero se il delegato vuoto iniziale è l'unica cosa nell'elenco. Non sarebbe ancora necessario prima fare una copia. Anche se penso che il caso che descrivi sarebbe comunque estremamente raro.
Daniel Earwicker,

Penso che stiamo fondendo le idee di delegati ed eventi qui. Se ho un evento Foo nella mia classe, quando gli utenti esterni chiamano MyType.Foo + = / - =, in realtà stanno chiamando i metodi add_Foo () e remove_Foo (). Tuttavia, quando faccio riferimento a Foo all'interno della classe in cui è definito, in realtà faccio riferimento direttamente al delegato sottostante, non ai metodi add_Foo () e remove_Foo (). E con l'esistenza di tipi come EventHandlerList, non c'è nulla di obbligatorio che il delegato e l'evento si trovino nello stesso posto. Questo è ciò che intendevo con il paragrafo "Ricorda" nella mia risposta.
Levi,

(cont.) Ammetto che si tratta di un progetto confuso, ma l'alternativa potrebbe essere stata peggio. Poiché alla fine tutto ciò che hai è un delegato - potresti fare riferimento direttamente al delegato sottostante, potresti ottenerlo da una raccolta, potresti istanziarlo al volo - potrebbe essere tecnicamente impossibile supportare qualsiasi cosa diversa dal "controllo per null "pattern.
Levi,

Mentre stiamo parlando di licenziare l'evento, non riesco a capire perché gli accessori di aggiunta / rimozione sono importanti qui.
Daniel Earwicker,

@Levi: non mi piace molto il modo in cui C # gestisce gli eventi. Se avessi i miei druther, al delegato sarebbe stato assegnato un nome diverso dall'evento. Dall'esterno di una classe, le uniche operazioni consentite su un nome di evento sarebbero +=e -=. All'interno della classe, le operazioni consentite includono anche l'invocazione (con controllo null incorporato), il test nullo l'impostazione su null. Per ogni altra cosa, si dovrebbe usare il delegato il cui nome sarebbe il nome dell'evento con un particolare prefisso o suffisso.
supercat,

0

per le applicazioni a thread singolo, si è corretti, questo non è un problema.

Tuttavia, se stai realizzando un componente che espone eventi, non vi è alcuna garanzia che un consumatore del tuo componente non passerà al multithreading, nel qual caso devi prepararti al peggio.

L'uso del delegato vuoto risolve il problema, ma provoca anche un calo delle prestazioni ad ogni chiamata all'evento e potrebbe avere implicazioni per GC.

Hai ragione, il consumatore deve annullare l'iscrizione affinché ciò accada, ma se ha superato la copia temporanea, considera il messaggio già in transito.

Se non usi la variabile temporanea e non usi il delegato vuoto e qualcuno annulla la sottoscrizione, otterrai un'eccezione di riferimento nulla, che è fatale, quindi penso che ne valga la pena.


0

Non l'ho mai considerato un grosso problema perché in genere proteggo solo da questo tipo di potenziale threading nei metodi statici (ecc.) Sui miei componenti riutilizzabili e non faccio eventi statici.

Sto sbagliando?


Se si alloca un'istanza di una classe con stato mutabile (campi che cambiano i loro valori) e quindi si consente a più thread di accedere contemporaneamente alla stessa istanza, senza utilizzare il blocco per proteggere quei campi dalla modifica di due thread contemporaneamente, si probabilmente sta sbagliando. Se tutti i tuoi thread hanno istanze separate (non condividono nulla) o tutti i tuoi oggetti sono immutabili (una volta allocati, i valori dei loro campi non cambiano mai), allora probabilmente stai bene.
Daniel Earwicker,

Il mio approccio generale è di lasciare la sincronizzazione al chiamante, tranne nei metodi statici. Se sono il chiamante, allora mi sincronizzerò a quel livello superiore. (ad eccezione di un oggetto il cui unico scopo è la gestione dell'accesso sincronizzato, ovviamente. :))
Greg D

@GregD che dipende dalla complessità del metodo e dai dati che utilizza. se influisce sui membri interni e decidi di correre in uno stato di thread / Tasked, sei molto ferito
Mickey Perlstein,

0

Collega tutti i tuoi eventi alla costruzione e lasciali soli. Il design della classe Delegate non può eventualmente gestire correttamente altri usi, come spiegherò nel paragrafo finale di questo post.

Prima di tutto, non ha senso tentare di intercettare una notifica di evento quando i gestori di eventi devono già prendere una decisione sincronizzata sul se / come rispondere alla notifica .

Tutto ciò che può essere notificato, deve essere avvisato. Se i gestori di eventi gestiscono correttamente le notifiche (ovvero hanno accesso a uno stato di applicazione autorevole e rispondono solo quando appropriato), allora andrà bene notificarle in qualsiasi momento e fidarsi che risponderanno correttamente.

L'unica volta in cui un gestore non dovrebbe essere avvisato che si è verificato un evento, è se l'evento in effetti non si è verificato! Quindi, se non si desidera che un gestore venga avvisato, interrompere la generazione degli eventi (ovvero disabilitare il controllo o qualunque cosa sia responsabile del rilevamento e della realizzazione dell'evento in primo luogo).

Onestamente, penso che la classe Delegate sia invalicabile. La fusione / transizione ad un MulticastDelegate è stato un enorme errore, perché ha effettivamente cambiato la (utile) definizione di un evento da qualcosa che accade in un singolo istante nel tempo, a qualcosa che accade in un arco di tempo. Tale modifica richiede un meccanismo di sincronizzazione che può logicamente ripiegarlo in un singolo istante, ma al MulticastDelegate manca tale meccanismo. La sincronizzazione dovrebbe comprendere l'intero periodo di tempo o l'istante in cui si verifica l'evento, in modo che quando un'applicazione prende la decisione sincronizzata di iniziare a gestire un evento, termina di gestirlo completamente (a livello transazionale). Con la scatola nera che è la classe ibrida MulticastDelegate / Delegate, questo è quasi impossibile, quindiaderire all'uso di un singolo abbonato e / o implementare il proprio tipo di MulticastDelegate che ha un handle di sincronizzazione che può essere rimosso mentre la catena di gestori viene utilizzata / modificata . Lo sto raccomandando, perché l'alternativa sarebbe implementare la sincronizzazione / integrità delle transazioni in modo ridondante in tutti i gestori, il che sarebbe ridicolmente / inutilmente complesso.


[1] Non esiste un gestore di eventi utile che si verifica in "un singolo istante nel tempo". Tutte le operazioni hanno un periodo di tempo. Ogni singolo gestore può avere una sequenza non banale di passaggi da eseguire. Supportare un elenco di gestori non cambia nulla.
Daniel Earwicker,

[2] Tenere un lucchetto mentre un evento viene lanciato è una follia totale. Porta inevitabilmente a un punto morto. La sorgente estrae il blocco A, genera un evento, il sink elimina il blocco B, ora vengono mantenuti due blocchi. Cosa succede se un'operazione in un altro thread comporta la rimozione dei blocchi nell'ordine inverso? Come possono essere escluse tali combinazioni mortali quando la responsabilità per il bloccaggio è divisa tra componenti progettati / testati separatamente (che è l'intero punto degli eventi)?
Daniel Earwicker,

[3] Nessuna di queste problematiche riduce in alcun modo l'utilità pervasiva dei normali delegati / eventi multicast nella composizione a thread singolo dei componenti, specialmente nei framework GUI. Questo caso d'uso copre la stragrande maggioranza degli usi degli eventi. L'uso degli eventi in modo libero è discutibile; ciò non invalida in alcun modo il loro design o la loro ovvia utilità nei contesti in cui hanno senso.
Daniel Earwicker,

[4] Thread + eventi sincroni è essenzialmente un'aringa rossa. La comunicazione asincrona in coda è la strada da percorrere.
Daniel Earwicker,

[1] Non mi riferivo al tempo misurato ... Stavo parlando di operazioni atomiche, che si verificano logicamente istantaneamente ... e con ciò intendo che nient'altro che coinvolge le stesse risorse che stanno usando può cambiare mentre si sta verificando l'evento perché è serializzato con un lucchetto.
Triynko,

0

Dai un'occhiata qui: http://www.danielfortunov.com/software/%24daniel_fortunovs_adventures_in_software_development/2009/04/23/net_event_invocation_thread_safety Questa è la soluzione corretta e deve essere sempre utilizzata al posto di tutti gli altri soluzioni alternative.

“Puoi assicurarti che l'elenco di invocazioni interno abbia sempre almeno un membro inizializzandolo con un metodo anonimo do-nothing. Poiché nessuna parte esterna può avere un riferimento al metodo anonimo, nessuna parte esterna può rimuovere il metodo, quindi il delegato non sarà mai nullo ”- Programming .NET Components, 2nd Edition, di 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

Non credo che la domanda sia limitata al tipo di "evento" c #. Rimuovendo tale restrizione, perché non reinventare un po 'la ruota e fare qualcosa del genere?

Solleva il thread degli eventi in modo sicuro - best practice

  • Possibilità di iscriversi / annullare l'iscrizione a qualsiasi thread durante un rilancio (condizione di gara rimossa)
  • Sovraccarichi dell'operatore per + = e - = a livello di classe.
  • Delegato generico definito dal chiamante

0

Grazie per una discussione utile. Di recente ho lavorato su questo problema e ho reso la seguente classe un po 'più lenta, ma consente di evitare chiamate a oggetti disposti.

Il punto principale qui è che l'elenco di invocazioni può essere modificato anche se viene generato un evento.

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

E l'uso è:

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

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

Test

L'ho provato nel modo seguente. Ho un thread che crea e distrugge oggetti come questo:

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

In un Barcostruttore (un oggetto listener) mi iscrivo a SomeEvent(che è implementato come mostrato sopra) e annulla l'iscrizione 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;
    }

Inoltre ho un paio di thread che generano eventi in un ciclo.

Tutte queste azioni vengono eseguite simultaneamente: molti ascoltatori vengono creati e distrutti e l'evento viene lanciato contemporaneamente.

Se ci fossero condizioni di gara dovrei vedere un messaggio in una console, ma è vuoto. Ma se uso gli eventi clr come al solito lo vedo pieno di messaggi di avviso. Quindi, posso concludere che è possibile implementare eventi thread-safe in c #.

Cosa ne pensi?


Mi sembra abbastanza buono. Anche se penso che potrebbe essere possibile (in teoria) disposed = trueaccadere prima foo.SomeEvent -= Handlernella tua applicazione di test, producendo un falso positivo. Ma a parte questo, ci sono alcune cose che potresti voler cambiare. Vuoi davvero usarlo try ... finallyper i blocchi - questo ti aiuterà a rendere questo non solo sicuro dai thread, ma anche da interrompere. Per non parlare del fatto che potresti liberarti di quello sciocco try catch. E non stai controllando il delegato passato in Add/ Remove- potrebbe essere null(dovresti lanciare subito in Add/ Remove).
Luaan,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.