Zdarzenia C # i bezpieczeństwo wątków


237

AKTUALIZACJA

Od C # 6 odpowiedź na to pytanie brzmi:

SomeEvent?.Invoke(this, e);

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

Zawsze wykonaj kopię wydarzenia przed sprawdzeniem nulli odpaleniem. Eliminuje to potencjalny problem z wątkami, w których zdarzenie staje się nullw miejscu dokładnie między miejscem, w którym sprawdza się wartość zerową, a miejscem, w którym wydarzenie jest uruchamiane:

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

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

Zaktualizowany : z lektury na temat optymalizacji pomyślałem, że może to również wymagać zmienności członka zdarzenia, ale Jon Skeet stwierdza w swojej odpowiedzi, że CLR nie optymalizuje kopii.

Tymczasem, aby ten problem mógł 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...

Rzeczywistą sekwencją może być ta mieszanina:

// 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 OnTheEventbiegnie po tym, jak autor wypisał się z subskrypcji, a jednak po prostu wypisali się specjalnie, aby tego uniknąć. Z pewnością naprawdę potrzebna jest niestandardowa implementacja zdarzenia z odpowiednią synchronizacją w addi removeakcesoriach. Ponadto istnieje problem możliwego zakleszczenia, jeśli blokada zostanie przytrzymana podczas odpalenia zdarzenia.

Więc to jest programowanie Cargo Cult ? Wydaje się, że w ten sposób - 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ększej uwagi niż to, zanim będą mogły być użyte jako część wielowątkowego projektu . W związku z tym ludzie, którzy nie dbają o to, mogą równie dobrze zignorować tę radę - to po prostu nie jest problem w przypadku programów jednowątkowych, aw rzeczywistości, biorąc pod uwagę brak volatilewiększości przykładowego kodu online, porady mogą nie zawierać efekt w ogóle.

(I czy nie jest o wiele prostsze przypisanie pustego delegate { }miejsca do deklaracji członka, abyś nigdy nie musiał sprawdzać nullw pierwszej kolejności?)

Zaktualizowano:W przypadku, gdy nie było to jasne, zrozumiałem zamiar porady - aby uniknąć wyjątku zerowego odniesienia we wszystkich okolicznościach. Chodzi mi o to, że ten szczególny wyjątek odniesienia zerowego może wystąpić tylko wtedy, gdy inny wątek jest usuwany ze zdarzenia, a jedynym powodem tego jest zapewnienie, że żadne inne wywołania nie będą odbierane przez to zdarzenie, co wyraźnie nie jest osiągnięte tą techniką . Ukrywałbyś warunki wyścigu - lepiej byłoby je ujawnić! Ten wyjątek zerowy pomaga wykryć nadużycie komponentu. Jeśli chcesz, aby Twój komponent był chroniony przed nadużyciami, możesz pójść za przykładem WPF - zapisz identyfikator wątku w swoim konstruktorze, a następnie wyrzuć wyjątek, jeśli inny wątek spróbuje wejść w interakcję bezpośrednio z twoim komponentem. Albo zaimplementuj prawdziwie bezpieczny wątek (niełatwe zadanie).

Twierdzę więc, że samo wykonywanie tego idiomu kopiowania / sprawdzania jest programowaniem kultu, dodając bałagan i szum do twojego kodu. Rzeczywista ochrona przed innymi wątkami wymaga dużo więcej pracy.

Aktualizacja w odpowiedzi na posty na blogu Erica Lipperta:

Tak więc w modułach obsługi zdarzeń brakuje jednej ważnej rzeczy: „procedury obsługi zdarzeń muszą być niezawodne w obliczu bycia wywoływanym nawet po anulowaniu subskrypcji zdarzenia”, i oczywiście musimy tylko dbać o możliwość zdarzenia być delegatem null. Czy ten wymóg dotyczący procedur obsługi zdarzeń jest gdziekolwiek udokumentowany?

I tak: „Istnieją inne sposoby rozwiązania tego problemu; na przykład inicjowanie modułu obsługi w celu wykonania pustej akcji, która nigdy nie jest usuwana. Ale sprawdzanie wartości zerowej jest standardowym wzorcem”.

Pozostaje więc fragment mojego pytania: dlaczego jawnie zeruje „standardowy wzorzec”? Alternatywą jest przypisanie pustego delegata, wymaga jedynie = delegate {}dodania do deklaracji wydarzenia, a to eliminuje te małe stosy śmierdzącej ceremonii z każdego miejsca, w którym odbywa się wydarzenie. Łatwo byłoby upewnić się, że pusty delegat jest tani do utworzenia. A może wciąż czegoś brakuje?

Z pewnością musi tak być (jak sugerował Jon Skeet), to tylko rada .NET 1.x, która nie wygasła, tak jak powinna była to zrobić w 2005 roku?


4
Pytanie to pojawiło się w wewnętrznej dyskusji jakiś czas temu; Od pewnego czasu zamierzam to blogować. Mój post na ten temat jest tutaj: Wydarzenia i wyścigi
Eric Lippert,

3
Stephen Cleary ma artykuł CodeProject, który analizuje ten problem, i podsumowuje ogólny cel, że „bezpieczne dla wątków” rozwiązanie nie istnieje. Zasadniczo to od inicjatora zdarzenia zależy, czy delegat nie ma wartości zerowej, oraz od procedury obsługi zdarzenia, która będzie w stanie obsłużyć wywołanie po anulowaniu subskrypcji.
rkagerer

3
@rkagerer - w rzeczywistości drugi problem czasami musi być rozwiązany przez moduł obsługi zdarzeń, nawet jeśli wątki nie są zaangażowane. Może się to zdarzyć, jeśli jeden moduł obsługi zdarzeń powie innemu modułowi obsługi, aby anulował subskrypcję aktualnie obsługiwanego zdarzenia, ale ten drugi subskrybent i tak odbierze zdarzenie (ponieważ anulowano subskrypcję w trakcie obsługi).
Daniel Earwicker

3
Dodanie subskrypcji do zdarzenia z zerową liczbą subskrybentów, usunięcie jedynej subskrypcji dla zdarzenia, wywołanie zdarzenia z zerową liczbą subskrybentów i wywołanie zdarzenia z dokładnie jednym subskrybentem, wszystkie operacje są znacznie szybsze niż w przypadku scenariuszy dodawania / usuwania / wywoływania obejmujących inne liczby subskrybenci. Dodanie fikcyjnego delegata spowalnia powszechny przypadek. Prawdziwy problem z C # polega na tym, że jego twórcy postanowili EventName(arguments)bezwarunkowo wywołać delegata zdarzenia, zamiast zmuszać go do wywoływania delegata tylko w przypadku wartości innej niż null (nie rób nic, jeśli wartość jest pusta).
supercat

Odpowiedzi:


100

JIT nie może przeprowadzić optymalizacji, o której mówisz w pierwszej części, ze względu na warunek. Wiem, że jakiś czas temu zostało to podniesione jako widmo, ale nie jest ważne. (Sprawdziłem to z Joe Duffy lub Vance Morrison jakiś czas temu; nie pamiętam, który.)

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

I tak, z pewnością warunki wyścigu - ale zawsze będą. Załóżmy, że po prostu zmieniamy kod na:

TheEvent(this, EventArgs.Empty);

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

Użycie pustego delegata z pewnością pozwala uniknąć sprawdzania nieważności, ale nie naprawia warunków wyścigu. Nie gwarantuje również, że zawsze „widzisz” najnowszą wartość zmiennej.


4
„Współbieżne programowanie w systemie Windows” Joe Duffy obejmuje aspekt pytania dotyczący optymalizacji JIT i modelu pamięci; patrz code.logos.com/blog/2008/11/events_and_threads_part_4.html
Bradley Grainger

2
Zaakceptowałem to na podstawie komentarza o tym, że „standardowa” rada jest wcześniejsza niż C # 2 i nie słyszę, żeby ktoś temu zaprzeczał. Jeśli tworzenie argumentów zdarzeń nie jest naprawdę drogie, wystarczy umieścić „= delegate {}” na końcu deklaracji zdarzenia, a następnie wywołać zdarzenia bezpośrednio, jakby były metodami; nigdy nie przypisuj im wartości null. (Inne rzeczy, które wprowadziłem, aby upewnić się, że moduł obsługi nie jest wywoływany po usunięciu z listy, to wszystko było nieistotne i nie można tego zapewnić, nawet w przypadku kodu jednowątkowego, np. Jeśli moduł obsługi 1 prosi moduł obsługi 2 o usunięcie, moduł obsługi 2 nadal będzie wywoływany dalej.)
Daniel Earwicker

2
Jedynym problemem (jak zawsze) są struktury, w których nie można zagwarantować, że zostaną utworzone w instancjach z wartościami innymi niż null w swoich elementach członkowskich. Ale struktury są do kitu.
Daniel Earwicker

1
O pustym uczestniku zobacz także to pytanie: stackoverflow.com/questions/170907/… .
Vladimir

2
@ Tony: Nadal istnieje zasadniczo wyścig między czymś, kto subskrybuje / anuluje subskrypcję, a delegatem jest wykonywany. Twój kod (po krótkim przejrzeniu) zmniejsza ten warunek wyścigu, pozwalając, aby subskrypcja / rezygnacja zaczęły obowiązywać podczas jego podwyższania, ale podejrzewam, że w większości przypadków, gdy normalne zachowanie nie jest wystarczająco dobre, to też nie jest.
Jon Skeet

52

Widzę wielu ludzi zmierzających w kierunku rozszerzenia metody robienia 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 podniesienia zdarzenia ...

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

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


9
Podoba mi się składnia, ale bądźmy szczerzy ... to nie rozwiązuje problemu z nieaktualnym modułem obsługi wywoływanym nawet po jego niezarejestrowaniu. To rozwiązuje tylko problem zerowania dereferencji. Chociaż podoba mi się składnia, pytam, czy jest ona naprawdę lepsza niż: public event EventHandler <T> MyEvent = delete {}; ... MyEvent (this, new MyEventArgs ()); Jest to również rozwiązanie o bardzo niskim tarciu, które lubię ze względu na swoją prostotę.
Simon Gillbee

@ Simon Widzę różnych ludzi mówiących na ten temat różne rzeczy. Przetestowałem to i to, co zrobiłem, wskazuje mi, że rozwiązuje to problem z zerowym modułem obsługi. Nawet jeśli oryginalny Sink wyrejestruje się ze zdarzenia po procedurze obsługi! = Kontrola zerowa, zdarzenie jest nadal zgłaszane i nie jest zgłaszany żaden wyjątek.
JP Alioto


1
+1. Właśnie napisałem tę metodę, zacząłem myśleć o bezpieczeństwie nici, przeprowadziłem badania i natknąłem się na to pytanie.
Niels van der Rest

Jak można to wywołać z VB.NET? Czy też „RaiseEvent” obsługuje już scenariusze wielowątkowe?

35

„Dlaczego jawnie zeruje„ standardowy wzorzec ”?”

Podejrzewam, że powodem tego może być fakt, że kontrola zerowa jest bardziej wydajna.

Jeśli zawsze subskrybujesz pustego uczestnika wydarzeń podczas ich tworzenia, pojawią się pewne koszty ogólne:

  • Koszt budowy pustego delegata.
  • Koszt budowy łańcucha delegatów do jego przechowywania.
  • Koszt wywołania bezcelowego delegata za każdym razem, gdy wydarzenie jest podnoszone.

(Należy pamiętać, że formanty interfejsu użytkownika często zawierają dużą liczbę zdarzeń, z których większość nigdy nie jest subskrybowana. Konieczność utworzenia fałszywego subskrybenta dla każdego zdarzenia, a następnie wywołania go, prawdopodobnie byłby znaczącym spadkiem wydajności).

Przeprowadziłem pobieżne testy wydajności, aby zobaczyć wpływ podejścia „subskrybuj-opróżnij-deleguj”, 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

Należy zauważyć, że w przypadku zero lub jednego subskrybenta (wspólne dla kontrolek interfejsu użytkownika, w których zdarzeń jest mnóstwo), zdarzenie wstępnie zainicjowane pustym delegatem jest znacznie wolniejsze (ponad 50 milionów iteracji ...)

Aby uzyskać więcej informacji i kod źródłowy, odwiedź ten wpis na blogu dotyczący bezpieczeństwa wątku wywołania zdarzenia .NET, który opublikowałem dzień przed zadaniem tego pytania (!)

(Moja konfiguracja testu może być wadliwa, więc pobierz kod źródłowy i sprawdź go sam. Wszelkie uwagi są mile widziane.)


8
Myślę, że w swoim wpisie na blogu robisz kluczowy punkt: nie musisz martwić się konsekwencjami dotyczącymi wydajności, dopóki nie stanie się wąskim gardłem. Dlaczego ten brzydki sposób jest zalecany? Gdybyśmy chcieli przedwczesnej optymalizacji zamiast jasności, używalibyśmy asemblera - więc moje pytanie pozostaje, i myślę, że prawdopodobną odpowiedzią jest to, że rada po prostu wyprzedza anonimowych delegatów, a kultura ludzka zajmuje dużo czasu, aby zmienić stare porady, takie jak w słynnej „historii pieczonego garnka”.
Daniel Earwicker

13
A twoje liczby bardzo dobrze to potwierdzają: koszty ogólne sprowadzają się do zaledwie dwóch i pół NANOSECONDS (!!!) na każde podniesione wydarzenie (przed inicjacją vs. klasyczne zero). Byłoby to niewykrywalne w prawie aplikacjach z prawdziwą pracą do wykonania, ale biorąc pod uwagę, że zdecydowana większość użycia zdarzeń odbywa się w ramach GUI, należy porównać to z kosztem odświeżenia części ekranu w Winforms itp., Więc staje się jeszcze bardziej niewidoczny w potopie prawdziwej pracy procesora i czekaniu na zasoby. W każdym razie dostajesz ode mnie +1 za ciężką pracę. :)
Daniel Earwicker

1
@DanielEarwicker powiedział słusznie, zmusiłeś mnie do bycia wierzącym w publicznym wydarzeniu WrapperDoneHandler OnWrapperDone = (x, y) => {}; Model.
Mickey Perlstein,

1
Dobrze byłoby także ustalić czas dla pary Delegate.Combine/ Delegate.Removepary w przypadkach, gdy zdarzenie ma zero, jednego lub dwóch subskrybentów; jeśli ktoś wielokrotnie dodaje i usuwa tę samą instancję delegowaną, różnice kosztów między przypadkami będą szczególnie wyraźne, ponieważ Combinema szybkie zachowanie w przypadku specjalnych przypadków, gdy jeden z argumentów jest null(po prostu zwraca drugi), i Removejest bardzo szybki, gdy oba argumenty są równe (wystarczy zwrócić null).
supercat

12

Naprawdę podobała mi się ta lektura - nie! Mimo że potrzebuję go do pracy z funkcją C # zwaną eventami!

Dlaczego nie naprawić tego w kompilatorze? Wiem, że są stwardnienie rozsiane ludzie, którzy czytają te posty, więc proszę nie podpalaj tego!

1 - problem Null ) Dlaczego nie sprawić, by zdarzenia były .Puste zamiast null w pierwszej kolejności? Ile wierszy kodu zostałoby zapisanych dla kontroli zerowej lub konieczności naklejenia = delegate {}na deklarację? Pozwól kompilatorowi obsługiwać pustą wielkość liter, IE nic nie rób! Jeśli to wszystko ma znaczenie dla twórcy wydarzenia, mogą sprawdzić, czy jest pusta i zrobić z nią wszystko, co im zależy! W przeciwnym razie wszystkie kontrole zerowe / dodawanie delegatów to hacki wokół problemu!

Szczerze mówiąc, mam już dość robienia tego z każdym wydarzeniem - znanym też jako kod bojlera!

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 ze stanem wyścigu ) Przeczytałem post na blogu Erica, zgadzam się, że H (przewodnik) powinien sobie poradzić, gdy sam się odreferentuje, ale czy nie można uczynić zdarzenia niezmiennym / bezpiecznym dla wątku? IE, ustaw flagę blokady podczas jej tworzenia, aby za każdym razem, gdy jest wywoływana, blokowała wszystkie subskrybujące i nie subskrybujące go podczas jego wykonywania?

Wniosek ,

Czy współczesne języki nie powinny dla nas rozwiązywać takich problemów?


Zgadzam się, powinna być lepsza obsługa tego w kompilatorze. Do tego czasu stworzyłem aspekt PostSharp, który robi to w kroku po kompilacji . :)
Steven Jeuris

4
Blokowanie żądań subskrypcji / rezygnacji z subskrypcji wątku podczas oczekiwania na zakończenie dowolnego kodu zewnętrznego jest o wiele gorsze niż to, że subskrybenci odbierają zdarzenia po anulowaniu subskrypcji, zwłaszcza że ten ostatni „problem” można łatwo rozwiązać , po prostu sprawdzając flagi zdarzeń, aby sprawdzić, czy nadal są zainteresowani otrzymaniem wydarzenia, ale impasy wynikające z poprzedniego projektu mogą być trudne.
supercat

@supercat. Imo, „znacznie gorszy” komentarz jest dość zależny od aplikacji. Kto nie chciałby bardzo rygorystycznego blokowania bez dodatkowych flag, jeśli jest to opcja? Zakleszczenie powinno wystąpić tylko wtedy, gdy wątek obsługi zdarzenia czeka na kolejny wątek (czyli subskrybuje / wypisuje się), ponieważ blokady są ponownie dołączane do tego samego wątku, a subskrypcja / anulacja subskrypcji w oryginalnej procedurze obsługi zdarzeń nie zostałaby zablokowana. Jeśli w ramach modułu obsługi zdarzeń czeka wątek krzyżowy, który byłby częścią projektu, wolałbym przerobić. Pochodzę z aplikacji po stronie serwera, która ma przewidywalne wzorce.
crokusek

2
@crokusek: Analiza wymagana do wykazania, że system jest wolny od impasu, jest łatwa, jeśli na ukierunkowanym wykresie nie byłoby cykli łączących każdą blokadę ze wszystkimi blokadami, które mogą być potrzebne podczas jej trzymania [brak cykli świadczy o tym, że system bez impasu]. Zezwolenie na wywołanie dowolnego kodu, gdy blokada jest przytrzymana, utworzy przewagę na wykresie „być może potrzebnym” od tego zamka do dowolnego zamka, który może uzyskać dowolny kod (nie całkiem każdy zamek w systemie, ale niedaleko od niego ). Konsekwentne istnienie cykli nie oznaczałoby, że nastąpi impas, ale ...
supercat,

1
... znacznie podniosłoby poziom analizy niezbędnej do udowodnienia, że ​​nie może.
supercat

8

W wersji C # 6 i nowszej kod można uprościć za pomocą nowego ?.operatora, jak w:

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

Oto dokumentacja MSDN.


5

Według Jeffreya Richtera w książce CLR przez 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ę referencyjną. Aby uzyskać więcej informacji, zobacz sekcję Wydarzenie w książce.


Być może brakuje mi czegoś, ale Interlocked.CompareExchange zgłasza wyjątek NullReferenceException, jeśli jego pierwszym argumentem jest null, co jest dokładnie tym, czego chcemy uniknąć. msdn.microsoft.com/en-us/library/bb297966.aspx
Kniganapolke

1
Interlocked.CompareExchangenie powiedzie się, jeśli zostanie w jakiś sposób przekazane zero ref, ale to nie to samo, co przekazanie refdo miejsca przechowywania (np. NewMail), które istnieje i które początkowo zawiera odwołanie zerowe.
supercat

4

Używam tego wzorca projektowego, aby upewnić się, że procedury obsługi zdarzeń nie są wykonywane po anulowaniu subskrypcji. Jak dotąd 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);
  }
}

Obecnie pracuję głównie z Mono na Androida, a Android nie lubi, gdy próbujesz zaktualizować widok po wysłaniu jego aktywności w tle.


Właściwie widzę, że ktoś inny używa tutaj bardzo podobnego wzorca: stackoverflow.com/questions/3668953/…
Ash

2

Ta praktyka nie polega na egzekwowaniu określonej kolejności operacji. W rzeczywistości chodzi o unikanie wyjątku odniesienia zerowego.

Rozumowanie ludzi dbających o wyjątek zerowy, a nie stan rasy, wymagałoby głębokich badań psychologicznych. Myślę, że ma to coś wspólnego z tym, że naprawienie problemu zerowego odniesienia jest znacznie łatwiejsze. Po rozwiązaniu problemu zawieszają duży kod „Wykonane misje” na kodzie i rozpakowują kombinezon lotniczy.

Uwaga: ustalenie warunków wyścigu prawdopodobnie wymaga użycia synchronicznego toru flagi, czy przewodnik powinien biec


Nie proszę o rozwiązanie tego problemu. Zastanawiam się, dlaczego istnieje szeroko rozpowszechniona rada, aby rozpylać dodatkowy bałagan w kodzie podczas odpalania zdarzeń, gdy pozwala to uniknąć zerowego wyjątku, gdy istnieją trudne do wykrycia warunki wyścigu, które nadal będą istnieć.
Daniel Earwicker

1
Cóż, o to mi chodziło. Nie dbają o stan wyścigu. Dbają tylko o wyjątek odniesienia zerowego. Zmienię to w mojej odpowiedzi.
dss539

Chodzi mi o to: dlaczego miałoby sens dbać o wyjątek zerowy, a nie dbać o stan wyścigu?
Daniel Earwicker

4
Należy odpowiednio przygotować procedurę obsługi zdarzeń, aby poradzić sobie z faktem, że każde konkretne żądanie podniesienia zdarzenia, którego przetwarzanie może pokrywać się z żądaniem dodania lub usunięcia, może, ale nie musi, spowodować podniesienie zdarzenia, które było dodawane lub usuwane. Programiści nie dbają o stan wyścigu, ponieważ w odpowiednio napisanym kodzie nie ma znaczenia, kto wygra .
supercat

2
@ dss539: Podczas gdy można zaprojektować strukturę zdarzeń, która blokowałaby żądania rezygnacji z subskrypcji, dopóki nie zakończy się wywołanie zdarzenia oczekującego, taki projekt uniemożliwiłby żadnemu zdarzeniu (nawet coś w rodzaju Unloadzdarzenia) bezpieczne anulowanie subskrypcji obiektu na inne zdarzenia. Paskudny. Lepiej jest po prostu powiedzieć, że żądania rezygnacji z subskrypcji spowodują, że zostaną ostatecznie anulowane, a subskrybenci wydarzenia powinni sprawdzić, kiedy są wywoływani, czy jest coś dla nich przydatnego.
supercat

1

Jestem więc trochę spóźniony na imprezę tutaj. :)

Jeśli chodzi o użycie wzorca zerowego zamiast wzorca zerowego do reprezentowania zdarzeń bez subskrybentów, rozważ ten scenariusz. Musisz wywołać zdarzenie, ale zbudowanie obiektu (EventArgs) nie jest trywialne, aw typowym przypadku twoje zdarzenie nie ma subskrybentów. Byłoby dla ciebie korzystne, gdybyś mógł zoptymalizować swój kod, aby sprawdzić, czy w ogóle masz subskrybentów, zanim podejmiesz wysiłek przetwarzania w celu zbudowania argumentów i wywołania zdarzenia.

Mając to na uwadze, rozwiązaniem jest powiedzenie „cóż, zero subskrybentów jest reprezentowane przez zero”. Następnie po prostu wykonaj zerową kontrolę przed wykonaniem drogiej operacji. Przypuszczam, że innym sposobem na to byłoby posiadanie właściwości Count typu Delegate, więc wykonałbyś kosztowną operację tylko wtedy, gdy myDelegate.Count> 0. Korzystanie z właściwości Count jest dość przyjemnym wzorcem, który rozwiązuje pierwotny problem umożliwienia optymalizacji, a także ma tę przyjemną właściwość, że można ją wywoływać bez powodowania wyjątku NullReferenceException.

Należy jednak pamiętać, że ponieważ delegaci są typami referencyjnymi, mogą mieć wartość zerową. Być może po prostu nie było dobrego sposobu na ukrycie tego faktu pod przykryciem i obsługę tylko wzorca zerowego obiektu dla zdarzeń, więc alternatywa mogła zmusić programistów do sprawdzenia zarówno zerowych, jak i zerowych subskrybentów. To byłoby jeszcze brzydsze niż obecna sytuacja.

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


Zakładam, że masz na myśli „użycie pustego delegata zamiast ...” Możesz już robić to, co sugerujesz, z wydarzeniem zainicjowanym dla pustego delegata. Test (MyEvent.GetInvocationList (). Length == 1) będzie prawdziwy, jeśli początkowy pusty delegat jest jedyną rzeczą na liście. Nadal nie ma potrzeby, aby najpierw wykonać kopię. Chociaż myślę, że opisany przypadek i tak byłby niezwykle rzadki.
Daniel Earwicker

Myślę, że łączymy tutaj pomysły delegatów i wydarzeń. Jeśli mam zdarzenie Foo w mojej klasie, wtedy gdy użytkownicy zewnętrzni wywołują MyType.Foo + = / - =, w rzeczywistości wywołują metody add_Foo () i remove_Foo (). Jednak gdy odwołuję się do Foo z klasy, w której jest on zdefiniowany, tak naprawdę odnoszę się bezpośrednio do delegata bazowego, a nie metod add_Foo () i remove_Foo (). A przy istnieniu typów, takich jak EventHandlerList, nic nie nakazuje, aby delegat i wydarzenie były nawet w tym samym miejscu. To właśnie miałem na myśli w swojej odpowiedzi w akapicie „Pamiętaj”.
Levi

(cd.) Przyznaję, że jest to projekt mylący, ale alternatywa mogła być gorsza. Ponieważ w końcu wszystko, co masz, to delegat - możesz odwołać się bezpośrednio do delegata bazowego, możesz pobrać go z kolekcji, możesz utworzyć go w locie - technicznie niewykonalne może być wspieranie czegokolwiek innego niż „sprawdź, czy” wzór zerowy.
Levi

Gdy mówimy o odpaleniu wydarzenia, nie rozumiem, dlaczego tutaj dodawane / usuwane są tak ważne elementy.
Daniel Earwicker

@Levi: Naprawdę nie podoba mi się sposób, w jaki C # obsługuje zdarzenia. Gdybym miał moich druterów, delegat otrzymałby inną nazwę niż to wydarzenie. Spoza klasy jedynymi dozwolonymi operacjami na nazwie zdarzenia byłyby +=i -=. W obrębie klasy dozwolone operacje obejmowałyby również wywoływanie (z wbudowanym sprawdzaniem zerowym), testowanie nulllub ustawienie na null. Do czegokolwiek innego należałoby użyć delegata, którego nazwą będzie nazwa zdarzenia z określonym prefiksem lub sufiksem.
supercat

0

w przypadku aplikacji jednowątkowych masz rację, to nie jest problem.

Jeśli jednak tworzysz komponent, który ujawnia zdarzenia, nie ma gwarancji, że konsument Twojego komponentu nie przejdzie wielowątkowości, w takim przypadku musisz przygotować się na najgorsze.

Korzystanie z pustego uczestnika nie rozwiązuje problemu, ale powoduje także spadek wydajności przy każdym wywołaniu zdarzenia i może mieć wpływ na GC.

Masz rację, że konsument próbuje zrezygnować z subskrypcji, aby tak się stało, ale jeśli zdołali przejść przez kopię tymczasową, rozważ wiadomość już wysłaną.

Jeśli nie użyjesz zmiennej tymczasowej i nie użyjesz pustego elementu delegowanego, a ktoś wypisze się z subskrypcji, otrzymasz wyjątek o wartości zerowej, co jest śmiertelne, więc uważam, że warto go kosztować.


0

Nigdy tak naprawdę nie uważałem tego za poważny problem, ponieważ generalnie chronię tylko przed tego rodzaju potencjalną wadą gwintowania w metodach statycznych (itp.) Na moich komponentach wielokrotnego użytku i nie robię zdarzeń statycznych.

Czy robię to źle?


Jeśli przydzielisz instancję klasy ze zmiennym stanem (pola, które zmieniają ich wartości), a następnie umożliwisz kilku wątkom jednoczesny dostęp do tego samego wystąpienia, bez blokowania w celu ochrony tych pól przed modyfikacją przez dwa wątki w tym samym czasie, jesteś prawdopodobnie robi to źle. Jeśli wszystkie twoje wątki mają swoje osobne instancje (nic nie współużytkują) lub wszystkie twoje obiekty są niezmienne (po przydzieleniu wartości ich pól nigdy się nie zmieniają), to prawdopodobnie nic ci nie jest.
Daniel Earwicker

Moje ogólne podejście polega na pozostawieniu synchronizacji rozmówcy, z wyjątkiem metod statycznych. Jeśli dzwonię, zsynchronizuję się na wyższym poziomie. (z wyjątkiem obiektu, którego jedynym celem jest oczywiście dostęp zsynchronizowany. :))
Greg D

@GregD zależy to od stopnia złożoności metody i wykorzystywanych danych. jeśli wpływa to na członków wewnętrznych, a ty decydujesz się na bieganie w trybie wątków / Zadań, czeka cię wiele bólu
Mickey Perlstein,

0

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

Przede wszystkim nie ma sensu przechwytywać powiadomienia o zdarzeniu, gdy osoby obsługujące zdarzenia muszą już podjąć zsynchronizowaną decyzję o tym, czy / jak odpowiedzieć na powiadomienie .

Wszystko, co może zostać zgłoszone, powinno zostać zgłoszone. Jeśli procedury obsługi zdarzeń prawidłowo obsługują powiadomienia (tj. Mają dostęp do wiarygodnego stanu aplikacji i odpowiadają tylko w razie potrzeby), w każdej chwili będzie można je powiadomić i mieć pewność, że odpowiedzą poprawnie.

Jedynym momentem, w którym przewodnik nie powinien być powiadamiany o wystąpieniu zdarzenia, jest fakt, że zdarzenie faktycznie nie miało miejsca! Więc jeśli nie chcesz, aby program obsługiwał powiadomienia, przestań generować zdarzenia (tj. Wyłącz kontrolę lub cokolwiek odpowiedzialnego za wykrywanie i uruchamianie zdarzenia w pierwszej kolejności).

Szczerze mówiąc, uważam, że klasa Delegata jest nie do uratowania. Połączenie / przejście do MulticastDelegate było ogromnym błędem, ponieważ skutecznie zmieniło (przydatną) definicję zdarzenia z czegoś, co dzieje się w jednej chwili, na coś, co dzieje się w określonym czasie. Taka zmiana wymaga mechanizmu synchronizacji, który może logicznie zwinąć go z powrotem w jedną chwilę, ale MulticastDelegate nie ma takiego mechanizmu. Synchronizacja powinna obejmować cały okres lub moment, w którym zdarzenie ma miejsce, tak więc gdy aplikacja podejmie zsynchronizowaną decyzję o rozpoczęciu obsługi zdarzenia, zakończy je całkowicie (transakcyjnie). Z czarną skrzynką, która jest klasą hybrydową MulticastDelegate / Delegate, jest to prawie niemożliwe, więcstosuj się do jednego subskrybenta i / lub implementuj swój własny rodzaj MulticastDelegate, który ma uchwyt synchronizacji, który można wyjąć, gdy łańcuch obsługi jest używany / modyfikowany . Polecam to, ponieważ alternatywą byłoby nadmiarowe wdrożenie synchronizacji / integralności transakcyjnej we wszystkich programach obsługi, co byłoby absurdalnie / niepotrzebnie skomplikowane.


[1] Nie ma użytecznego modułu obsługi zdarzeń, który miałby miejsce „w jednej chwili”. Wszystkie operacje mają określony czas. Każdy pojedynczy moduł obsługi może mieć nietrywialną sekwencję kroków do wykonania. Obsługa listy programów obsługi niczego nie zmienia.
Daniel Earwicker

[2] Trzymanie zamka podczas wystrzelenia zdarzenia to totalne szaleństwo. Prowadzi to nieuchronnie do impasu. Źródło wyjmuje blokadę A, zdarzenie pożaru, zlew wyjmuje blokadę B, teraz dwie blokady są utrzymywane. Co się stanie, jeśli jakaś operacja w innym wątku spowoduje usunięcie blokad w odwrotnej kolejności? Jak można wykluczyć takie śmiertelne kombinacje, gdy odpowiedzialność za blokowanie jest podzielona na osobno zaprojektowane / przetestowane komponenty (co jest sednem wydarzeń)?
Daniel Earwicker

[3] Żaden z tych problemów w żaden sposób nie ogranicza wszechobecnej użyteczności normalnych delegatów / zdarzeń multiemisji w jednowątkowym składzie komponentów, szczególnie w ramach GUI. Ten przypadek użycia obejmuje ogromną większość zastosowań zdarzeń. Korzystanie ze zdarzeń w dowolny sposób ma wątpliwą wartość; w żaden sposób nie podważa to ich projektu ani ich oczywistej przydatności w kontekstach, w których mają sens.
Daniel Earwicker

[4] Wątki + zdarzenia synchroniczne to zasadniczo czerwony śledź. Kolejką jest komunikacja asynchroniczna w kolejce.
Daniel Earwicker

[1] Nie miałem na myśli mierzonego czasu ... Mówiłem o operacjach atomowych, które logicznie następują natychmiastowo ... i przez to nie mam na myśli, że nic innego, z czego korzystają te same zasoby, których używają, nie może się zmienić podczas wydarzenia ponieważ jest szeregowany z blokadą.
Triynko

0

Zajrzyj tutaj: http://www.danielfortunov.com/software/%24daniel_fortunovs_adventures_in_software_development/2009/04/23/net_event_invocation_thread_safety To jest prawidłowe 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ą„ nic nie rób ”. Ponieważ żadna strona zewnętrzna nie może odwoływać się do metody anonimowej, żadna strona zewnętrzna nie może usunąć tej metody, więc delegat nigdy nie będzie zerowy ”- Programowanie .NET Components, wydanie drugie, autorstwa 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

Nie sądzę, aby pytanie było ograniczone do typu „zdarzenie” c #. Usuwając to ograniczenie, dlaczego nie wynaleźć trochę koła i zrobić coś w tym stylu?

Podnieś wątek zdarzenia bezpiecznie - najlepsza praktyka

  • Możliwość subskrybowania / anulowania dowolnego wątku w trakcie podwyższenia (usunięto warunek wyścigu)
  • Przeciążenie operatora dla + = i - = na poziomie klasy.
  • Ogólny delegat zdefiniowany przez dzwoniącego

0

Dziękuję za przydatną dyskusję. Ostatnio pracowałem nad tym problemem i stworzyłem następującą klasę, która jest nieco wolniejsza, ale pozwala uniknąć wywołań do rozmieszczonych obiektów.

Najważniejsze jest to, że lista wywołań może być modyfikowana nawet w przypadku podniesienia zdarzenia.

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

W konstruktorze Bar(obiekt nasłuchujący) subskrybuję SomeEvent(który jest implementowany 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 podnoszą zdarzenia w pętli.

Wszystkie te czynności są wykonywane jednocześnie: wielu słuchaczy jest tworzonych i niszczonych, a wydarzenie jest uruchamiane w tym samym czasie.

Jeśli były warunki wyścigu, powinienem zobaczyć komunikat w konsoli, ale jest pusty. Ale jeśli używam zdarzeń clr jak zwykle, widzę je pełne komunikatów ostrzegawczych. Mogę więc stwierdzić, że można zaimplementować zdarzenia wątku bezpieczne w języku c #.

Co myślisz?


Wygląda mi wystarczająco dobrze. Chociaż myślę, że może to być (teoretycznie) możliwe, disposed = trueże stanie się to wcześniej foo.SomeEvent -= Handlerw twojej aplikacji testowej, dając fałszywie dodatni wynik. Ale oprócz tego jest kilka rzeczy, które możesz chcieć zmienić. Naprawdę chcesz używać try ... finallydo zamków - pomoże to uczynić to nie tylko bezpiecznym dla wątków, ale także bezpiecznym dla aborcji. Nie wspominając już o tym, że możesz się tego pozbyć try catch. I nie sprawdzasz, czy delegat przekazał Add/ Remove- może być null(powinieneś od razu wrzucić Add/ Remove).
Luaan
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.