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 null
i odpaleniem. Eliminuje to potencjalny problem z wątkami, w których zdarzenie staje się null
w 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 OnTheEvent
biegnie 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 add
i remove
akcesoriach. 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 volatile
wię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ć null
w 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?
źródło
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).Odpowiedzi:
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:
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.
źródło
Widzę wielu ludzi zmierzających w kierunku rozszerzenia metody robienia tego ...
To daje ładniejszą składnię do podniesienia zdarzenia ...
A także usuwa lokalną kopię, ponieważ jest ona przechwytywana w czasie wywołania metody.
źródło
„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:
(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:
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.)
źródło
Delegate.Combine
/Delegate.Remove
pary 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żCombine
ma szybkie zachowanie w przypadku specjalnych przypadków, gdy jeden z argumentów jestnull
(po prostu zwraca drugi), iRemove
jest bardzo szybki, gdy oba argumenty są równe (wystarczy zwrócić null).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!
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?
źródło
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.
źródło
Według Jeffreya Richtera w książce CLR przez C # , poprawną metodą jest:
Ponieważ wymusza kopię referencyjną. Aby uzyskać więcej informacji, zobacz sekcję Wydarzenie w książce.
źródło
Interlocked.CompareExchange
nie powiedzie się, jeśli zostanie w jakiś sposób przekazane zeroref
, ale to nie to samo, co przekazanieref
do miejsca przechowywania (np.NewMail
), które istnieje i które początkowo zawiera odwołanie zerowe.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.
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.
źródło
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
źródło
Unload
zdarzenia) 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.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.
źródło
+=
i-=
. W obrębie klasy dozwolone operacje obejmowałyby również wywoływanie (z wbudowanym sprawdzaniem zerowym), testowanienull
lub ustawienie nanull
. Do czegokolwiek innego należałoby użyć delegata, którego nazwą będzie nazwa zdarzenia z określonym prefiksem lub sufiksem.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ć.
źródło
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?
źródło
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.
źródło
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
źródło
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
źródło
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.
A użycie to:
Test
Przetestowałem to w następujący sposób. Mam wątek, który tworzy i niszczy takie obiekty:
W konstruktorze
Bar
(obiekt nasłuchujący) subskrybujęSomeEvent
(który jest implementowany jak pokazano powyżej) i wypisuję się zDispose
: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?
źródło
disposed = true
że stanie się to wcześniejfoo.SomeEvent -= Handler
w 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 ... finally
do 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
).