C# Events and Thread Safety

UPDATE

Od C # 6 odpowiedź na to pytanie brzmi:

SomeEvent?.Invoke(this, e);

Często słyszę/czytam następujące rady:

Zawsze rób kopię zdarzenia, zanim sprawdzisz, czy nie ma null i odpal je. Wyeliminuje to potencjalny problem z wątkiem, w którym zdarzenie staje się null w miejscu pomiędzy miejscem, w którym sprawdzasz wartość null, a miejscem, w którym uruchamiasz Zdarzenie:

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

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

Aktualizacja : myślałem z lektury o optymalizacjach że może to również wymagać, aby członek imprezy był zmienny, ale Jon Skeet stwierdza w swojej odpowiedzi, że CLR nie optymalizuje kopii.

Ale tymczasem, aby ten problem w ogóle wystąpił, inny wątek musiał zrobić coś takiego:

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

Rzeczywista sekwencja może być tą mieszaniną:

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

Chodzi o to, że OnTheEvent działa po tym, jak Autor się wypisał, a mimo to wypisał się specjalnie, aby tego uniknąć. Na pewno co jest naprawdę potrzebna jest niestandardowa implementacja zdarzeń z odpowiednią synchronizacją w accesorach add i remove. Ponadto istnieje problem możliwych blokad, jeśli blokada jest utrzymywana podczas wywoływania zdarzenia.

Więc to jest Programowanie kultu ładunku ? Wydaje się, że tak - wiele osób musi podjąć ten krok, aby chronić swój kod przed wieloma wątkami, podczas gdy w rzeczywistości wydaje mi się, że zdarzenia wymagają znacznie więcej uwagi niż to, zanim będą mogły być użyte jako część wielowątkowa konstrukcja. W związku z tym osoby, które nie dbają o tę dodatkową uwagę, równie dobrze mogą zignorować tę radę - po prostu nie jest to problem dla programów jednowątkowych, a w rzeczywistości, biorąc pod uwagę brak volatile w większości przykładów kodu online, porada może nie mieć żadnego wpływu.

(i czy nie jest o wiele prostsze przypisanie pustego delegate { } na deklaracji członkowskiej, aby nigdy nie trzeba było sprawdzać null w pierwszej kolejności?)

Aktualizacja: w razie nie było jasne, zrozumiałem zamiar porady - aby uniknąć zerowego wyjątku odniesienia we wszystkich okolicznościach. Chodzi mi o to, że ten szczególny wyjątek null reference może wystąpić tylko wtedy, gdy inny wątek jest usuwany ze zdarzenia, a jedynym powodem tego jest zapewnienie, że żadne dalsze wywołania nie będą odbierane przez to zdarzenie, co wyraźnie nie jest osiągnięte przez tę technikę. Ukrywałbyś stan rasy - lepiej byłoby to ujawnić! Ten wyjątek null pomaga wykryć nadużycie twojego komponentu. Jeśli chcesz, aby twój komponent był chroniony przed nadużyciami, możesz postępować zgodnie z przykładem WPF-przechowywać identyfikator wątku w konstruktorze, a następnie rzucać wyjątek, jeśli inny wątek spróbuje wejść w interakcję bezpośrednio z Twoim komponentem. Albo wdrożyć komponent naprawdę bezpieczny dla wątku (nie jest to łatwe zadanie).

Twierdzę więc, że samo zrobienie idiomu copy/check jest kultowym programowaniem, dodawaniem bałaganu i szumu do kodu. Aby faktycznie chronić przed innymi wątkami wymaga dużo więcej pracy.

Aktualizacja W odpowiedzi na posty na blogu Erica Lipperta:

Jest więc jedna rzecz, której mi brakowało w programach obsługi zdarzeń: "programy obsługi zdarzeń muszą być solidne w obliczu bycia wywoływanym nawet po tym, jak wydarzenie zostało anulowane", i oczywiście dlatego musimy dbać tylko o możliwość, że delegat wydarzenia jest null. czy wymóg dotyczący obsługi zdarzeń jest gdzieś udokumentowany?

I tak: "są inne sposoby rozwiązania tego problemu; na przykład Inicjowanie obsługi tak, aby miała pustą akcję, która nigdy nie jest usuwana. Ale sprawdzanie null jest standardowym wzorcem."

Pozostał więc fragment mojego pytania: dlaczego explicit-null-check jest "standardowym wzorcem"? alternatywa, przypisanie pustego delegata, wymaga dodania tylko = delegate {} do deklaracji wydarzenia, a to eliminuje te małe stosy śmierdzącej ceremonii z każdego miejsca, w którym wydarzenie jest podnoszone. Informatyka łatwo byłoby upewnić się, że pusty delegat jest tani do utworzenia instancji. A może wciąż coś przeoczyłem?

Z pewnością musi być tak, że (jak zasugerował Jon Skeet) jest to po prostu.NET 1.x porada, która nie wygasła, jak powinna być w 2005 roku?

Author: Daniel Earwicker, 2009-04-24

15 answers

JIT nie może wykonać optymalizacji, o której mówisz w pierwszej części, ze względu na warunek. Wiem, że to zostało podniesione jako widmo jakiś czas temu, ale to nie jest ważne. (Sprawdziłam to jakiś czas temu u Joe Duffy 'ego lub Vance' a Morrisona; nie pamiętam którego.)

Bez zmiennego modyfikatora możliwe jest, że lokalna kopia będzie nieaktualna, ale to wszystko. Nie spowoduje NullReferenceException.

I tak, na pewno jest stan rasy - ale tam zawsze będzie. Załóżmy, że po prostu zmienimy kod na:

TheEvent(this, EventArgs.Empty);

Załóżmy teraz, że lista wywołań dla tego delegata ma 1000 wpisów. Jest całkowicie możliwe, że akcja na początku listy zostanie wykonana, zanim inny wątek anuluje subskrypcję obsługi na końcu listy. Jednak ten handler nadal będzie wykonywany, ponieważ będzie to nowa lista. (Delegaci są niezmienni.) Z tego co widzę jest to nieuniknione.

Użycie pustego delegata z pewnością unika Kontrola nieważności, ale nie naprawia stanu rasy. Nie gwarantuje to również, że zawsze "widzisz" najnowszą wartość zmiennej.

 97
Author: Jon Skeet,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-04-24 15:53:30

Widzę wiele osób idących w kierunku metody rozszerzania tego ...

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

To daje ładniejszą składnię do wywołania zdarzenia ...

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

A także usuwa lokalną kopię, ponieważ jest przechwytywana w czasie wywołania metody.

 50
Author: JP Alioto,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-04-24 15:56:33

"Dlaczego explicit-null-check 'standard pattern'?"

Podejrzewam, że powodem tego może być to, że null-check jest bardziej wydajny.

Jeśli zawsze subskrybujesz pustego delegata do swoich wydarzeń podczas ich tworzenia, będą pewne koszty ogólne:

  • koszt budowy pustego delegata.
  • koszt budowy łańcucha delegatów do jego przechowywania.
  • koszt wywołania bezsensownego delegata za każdym razem, gdy zdarzenie jest podniesiony.

(Należy pamiętać, że kontrolki interfejsu często mają dużą liczbę zdarzeń, z których większość nigdy nie jest subskrybowana. Konieczność stworzenia pozornego Abonenta dla każdego zdarzenia, a następnie wywołania go, prawdopodobnie byłaby znaczącym uderzeniem W wydajność.)

Zrobiłem pobieżne testy wydajności, aby zobaczyć wpływ podejścia subscribe-empty-delegate, a oto moje wyniki: {]}

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

Zauważ, że w przypadku zero lub jeden abonentów (wspólne dla kontrolek UI, gdzie zdarzenia są obfite), Zdarzenie wstępnie zainicjowane pustym delegatem jest wyraźnie wolniejsze (ponad 50 milionów iteracji...)

Aby uzyskać więcej informacji i kodu źródłowego, odwiedź ten wpis na blogu . Net Event invocation thread safety , który opublikowałem dzień przed tym pytaniem (!)

(moja konfiguracja testowa może być wadliwa, więc możesz pobrać kod źródłowy i sprawdzić go samodzielnie. Wszelkie opinie są bardzo mile widziane.)

 31
Author: Daniel Fortunov,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-05-11 06:15:16

Naprawdę podobała mi się ta lektura - nie! Mimo, że potrzebuję go do pracy z funkcją c# o nazwie events!

Dlaczego nie naprawić tego w kompilatorze? Wiem, że są osoby z MS, które czytają te posty, więc proszę nie palić tego!

1 - Problem Null ) Dlaczego nie uczynić zdarzeń takimi .Pusty zamiast null? Ile linii kodu zostałoby zapisanych do sprawdzenia null lub konieczności przyklejenia = delegate {} do deklaracji? Niech kompilator zajmie się pustą sprawą, czyli nic nie robi! Jeśli to wszystko ma znaczenie dla twórcy wydarzenia, mogą sprawdzić .Opróżnij i zrób z tym, co im zależy! W przeciwnym razie wszystkie null checks / delegate adds to hacki wokół problemu!

Szczerze mówiąc jestem zmęczony koniecznością robienia tego z każdym wydarzeniem-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 - Problem z race condition ) przeczytałem Post Erica na blogu, zgadzam się, że H (handler) powinien sobie poradzić, gdy się wycofa, ale czy nie można zrobić zdarzenia niezmiennego / wątku bezpiecznego? IE, Ustaw blokadę flaga na jego utworzenie, tak, że kiedykolwiek jest wywoływany, to blokuje wszystkie subskrybowanie i un-subskrybowanie do niego podczas jego wykonywania?

Wnioski ,

Czy współczesne języki nie powinny rozwiązywać takich problemów?
 10
Author: Chuck Savage,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-04-27 19:47:35

Według Jeffreya Richtera w książce CLR via C#, poprawną metodą jest:

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

Ponieważ wymusza kopię odniesienia. Aby uzyskać więcej informacji, zobacz jego dział wydarzenia w książce.

 5
Author: alyx,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-11-11 00:18:03

Używam tego wzorca projektowego, aby upewnić się, że procedury obsługi zdarzeń nie są wykonywane po ich wypisaniu. Do tej pory działa całkiem dobrze, chociaż nie próbowałem żadnego profilowania wydajności.

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

W dzisiejszych czasach pracuję głównie z Mono dla Androida, a Android nie podoba się, gdy próbujesz zaktualizować Widok po wysłaniu jego aktywności do tła.

 4
Author: Ash,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-07-16 21:49:43

Ta praktyka nie polega na egzekwowaniu określonej kolejności operacji. Chodzi o unikanie wyjątku null reference.

Rozumowanie ludzi dbających o wyjątek null reference, a nie o stan rasy, wymagałoby głębokich badań psychologicznych. Myślę, że ma to coś wspólnego z faktem, że naprawienie problemu null reference jest znacznie łatwiejsze. Gdy to naprawią, powieszą duży baner "Misja zakończona" na swoim kodzie i rozpakują swój lot Garnitur.

Uwaga: naprawienie warunku wyścigu prawdopodobnie polega na użyciu synchronicznego toru flagi, czy opiekun powinien uruchomić

 1
Author: dss539,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-04-24 17:19:43

Więc jestem trochę spóźniony na imprezę tutaj. :)

Jeśli chodzi o użycie null zamiast wzorca obiektu null do reprezentowania zdarzeń bez subskrybentów, rozważ ten scenariusz. Musisz wywołać zdarzenie, ale konstruowanie obiektu (EventArgs) jest nietrywialne i w powszechnym przypadku twoje zdarzenie nie ma subskrybentów. Byłoby to korzystne dla Ciebie, jeśli można zoptymalizować kod, aby sprawdzić, czy masz żadnych subskrybentów w ogóle przed podjęciem wysiłku przetwarzania do konstruowania argumenty i wywołanie zdarzenia.

Mając to na uwadze, rozwiązaniem jest stwierdzenie " cóż, zero subskrybentów jest reprezentowane przez null."Następnie po prostu wykonaj kontrolę zerową przed wykonaniem kosztownej operacji. Przypuszczam, że innym sposobem na zrobienie tego byłoby posiadanie właściwości Count na typie delegata, więc wykonałbyś tylko kosztowną operację, jeśli myDelegate.Count > 0. Korzystanie z właściwości Count jest dość ładnym wzorcem, który rozwiązuje pierwotny problem pozwalający na optymalizację i ma również właściwość nice polegającą na tym, że można ją wywoływać bez powodowania wyjątku NullReferenceException.

Należy jednak pamiętać, że ponieważ delegaty są typami referencyjnymi, mogą być null. Być może po prostu nie było dobrego sposobu na ukrycie tego faktu pod przykrywką i wspieranie tylko wzorca obiektu null dla zdarzeń, więc alternatywa mogła zmuszać deweloperów do sprawdzania zarówno dla null, jak i dla zerowych subskrybentów. To byłoby jeszcze brzydsze niż obecne sytuacja.

Uwaga: to czysta spekulacja. Nie jestem zaangażowany w języki. NET lub CLR.

 1
Author: Levi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-05-01 05:33:16

W C # 6 i wyżej, kod można uprościć używając nowego {[0] } jak w TheEvent?.Invoke(this, EventArgs.Empty);

Https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/null-conditional-operators{[6]

 1
Author: Phil1970,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-08-29 23:47:18

Dla aplikacji z pojedynczym gwintem, jesteś correc to nie jest problem.

Jednakże, jeśli tworzysz komponent, który eksponuje zdarzenia, nie ma gwarancji, że konsument twojego komponentu nie przejdzie wielowątkowości, w którym to przypadku musisz przygotować się na najgorsze.

Użycie pustego delegata rozwiązuje problem, ale również powoduje uderzenie wydajności przy każdym wywołaniu zdarzenia i może mieć wpływ na GC.

Masz rację, że konsument trie Dto wypisać się, aby to się stało, ale jeśli udało im się przejść przez kopię tymczasową, a następnie rozważyć wiadomość już w tranzycie.

Jeśli nie używasz zmiennej tymczasowej i nie używasz pustego delegata, a ktoś anuluje subskrypcję, dostajesz wyjątek null reference, który jest fatalny, więc myślę, że koszt jest tego wart.

 0
Author: Jason Coyne,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-04-24 15:46:10

Nigdy tak naprawdę nie uważałem tego za duży problem, ponieważ generalnie chronię tylko przed tego rodzaju potencjalną wadą wątku w metodach statycznych (etc) na moich komponentach wielokrotnego użytku i nie robię zdarzeń statycznych.

Czy robię to źle?

 0
Author: Greg D,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-04-24 17:31:31

Przełącz wszystkie wydarzenia na budowę i zostaw je w spokoju. Konstrukcja klasy delegata nie może obsługiwać poprawnie żadnego innego użycia, jak wyjaśnię w ostatnim akapicie tego postu.

Po pierwsze, nie ma sensu próbować przechwycić powiadomienie o zdarzeniu , gdy Obsługa zdarzenia {4]} musi już podjąć zsynchronizowaną decyzję o tym, czy/jak odpowiedzieć na powiadomienie .

Wszystko, co może być zgłoszone, powinno bądź powiadomiony. Jeśli Twoje podmioty zajmujące się obsługą zdarzeń prawidłowo obsługują powiadomienia (tzn. mają dostęp do autorytatywnego stanu aplikacji i odpowiadają tylko wtedy, gdy jest to właściwe), dobrze będzie powiadomić je w dowolnym momencie i zaufać, że odpowiedzą poprawnie.

Jedyny przypadek, kiedy handler nie powinien być powiadamiany o zdarzeniu, to jeśli zdarzenie w rzeczywistości nie miało miejsca! Więc jeśli nie chcesz, aby obsługa była powiadamiana, przestań generować zdarzenia (np. wyłącz kontrolkę lub cokolwiek innego odpowiada przede wszystkim za wykrycie i wywołanie zdarzenia).

Szczerze, myślę, że Klasa delegata jest nie do przyjęcia. Połączenie / przejście do MulticastDelegate było ogromnym błędem, ponieważ skutecznie zmieniło (użyteczną) definicję zdarzenia z czegoś, co dzieje się w jednej chwili w czasie, do czegoś, co dzieje się w czasie. Taka zmiana wymaga mechanizmu synchronizacji, który może logicznie zwinąć go z powrotem do jednej chwili, ale MulticastDelegate nie ma takiego mechanizmu. Synchronizacja powinna obejmować cały czas lub natychmiast zdarzenie ma miejsce, tak że gdy aplikacja podejmie zsynchronizowaną decyzję o rozpoczęciu obsługi zdarzenia, kończy obsługę go całkowicie (transakcyjnie). Z czarną skrzynką, która jest hybrydową klasą MulticastDelegate/Delegate, jest to prawie niemożliwe, więc stosuj się do używania pojedynczego abonenta i / lub zaimplementuj swój własny rodzaj MulticastDelegate, który ma uchwyt synchronizacji, który można wyjąć podczas używania / modyfikowania łańcucha obsługi. Polecam to, ponieważ alternatywą byłoby zaimplementowanie synchronizacji/transactional-integrity redundantnie we wszystkich swoich manipulatorach, co byłoby śmiesznie / niepotrzebnie skomplikowane.

 0
Author: Triynko,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2009-05-27 18:49:38

Proszę spojrzeć tutaj: http://www.danielfortunov.com/software/%24daniel_fortunovs_adventures_in_software_development/2009/04/23/net_event_invocation_thread_safety Jest to właściwe rozwiązanie i powinno być zawsze stosowane zamiast wszystkich innych obejść.

" możesz upewnić się, że wewnętrzna lista wywołań ma zawsze co najmniej jednego członka, inicjując ją anonimową metodą do-nothing. Ponieważ żadna strona zewnętrzna nie może mieć odniesienia do metody anonimowej, nie strona zewnętrzna może usunąć metodę, więc delegat nigdy nie będzie null" - Programowanie komponentów. NET, wydanie 2, Autor: 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
Author: Eli,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-04-26 16:28:30

Nie wierzę, że pytanie jest ograniczone do Typu c# "event". Usuwając to ograniczenie, dlaczego nie wymyślić trochę koła i zrobić coś wzdłuż tych linii?

Zwiększ bezpiecznie wątek zdarzeń-najlepsza praktyka

  • możliwość sub / wypisania się z dowolnego wątku podczas przebijania (wyścig warunek usunięty)
  • przeciążenia operatorów dla + = i - = na poziomie klasy.
  • Generic caller-defined delegate
 0
Author: crokusek,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:17:39

Dzięki za użyteczną dyskusję. Ostatnio pracowałem nad tym problemem i zrobiłem następującą klasę, która jest nieco wolniejsza, ale pozwala uniknąć wywołań do usuniętych obiektów.

Głównym punktem jest to, że lista wywołań może być modyfikowana nawet wtedy, gdy wystąpi zdarzenie.

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

A użycie to:

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

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

Test

Przetestowałem go w następujący sposób. Mam wątek który tworzy i niszczy takie obiekty:
var objects = Enumerable.Range(0, 1000).Select(x => new Bar(foo)).ToList();
Thread.Sleep(10);
objects.ForEach(x => x.Dispose());

In a Bar (słuchacz obiekt) konstruktor zapisuję się do SomeEvent (co jest zaimplementowane jak pokazano powyżej) i wypisuję się z 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;
    }

Mam też kilka wątków, które wywołują Zdarzenie w pętli.

Wszystkie te działania są wykonywane jednocześnie: wielu słuchaczy jest tworzonych i niszczonych, a zdarzenie jest wywoływane w tym samym czasie.

Gdyby były warunki wyścigu, powinienem zobaczyć wiadomość w konsoli, ale jest pusta. Ale jeśli używam zdarzeń clr jak zwykle widzę to pełne komunikatów ostrzegawczych. Więc, Ja można wywnioskować, że możliwe jest zaimplementowanie wątku safe events W c#.

Co o tym myślisz?
 0
Author: Tony,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-10-18 07:33:31