Od razu powiem, że ten artykuł będzie dotyczył nie wątków w szczególności, ale zdarzeń w kontekście wątków w .NET. Dlatego nie będę próbował poprawnie organizować wątków (ze wszystkimi blokami, wywołaniami zwrotnymi, anulowaniem itp.). Jest wiele artykułów na ten temat.
Wszystkie przykłady są napisane w C# dla frameworka w wersji 4.0 (w 4.6 wszystko jest nieco prostsze, ale wciąż jest wiele projektów w 4.0). Postaram się również trzymać C# w wersji 5.0.
Po pierwsze, chciałbym zauważyć, że są gotowi delegaci do systemu zdarzeń .Net, których gorąco polecam użyć zamiast wymyślać coś nowego. Na przykład często spotykałem się z następującymi 2 metodami organizowania wydarzeń.
Pierwsza metoda:
class WrongRaiser { public event Action<object> MyEvent; public event Action MyEvent2; }
Polecam ostrożnie korzystać z tej metody. Jeśli nie zuniwersalizujesz go, możesz w końcu napisać więcej kodu, niż się spodziewałeś. W związku z tym nie ustawi bardziej precyzyjnej struktury w porównaniu z poniższymi metodami.
Z mojego doświadczenia mogę powiedzieć, że używałem go, kiedy zaczynałem pracować z wydarzeniami i w konsekwencji zrobiłem z siebie głupca. Teraz nigdy bym do tego nie doszło.
Druga metoda:
class WrongRaiser { public event MyDelegate MyEvent; } class MyEventArgs { public object SomeProperty { get; set; } } delegate void MyDelegate(object sender, MyEventArgs e);
Ta metoda jest całkiem poprawna, ale jest dobra w szczególnych przypadkach, gdy poniższa metoda z jakichś powodów nie działa. W przeciwnym razie możesz mieć dużo monotonnej pracy.
A teraz spójrzmy, co już zostało stworzone na potrzeby wydarzeń.
Metoda uniwersalna:
class Raiser { public event EventHandler<MyEventArgs> MyEvent; } class MyEventArgs : EventArgs { public object SomeProperty { get; set; } }
Jak widać, posługujemy się tutaj uniwersalną klasą EventHandler. Oznacza to, że nie ma potrzeby definiowania własnego programu obsługi.
Kolejne przykłady przedstawiają metodę uniwersalną.
Rzućmy okiem na najprostszy przykład generatora zdarzeń.
class EventRaiser { int _counter; public event EventHandler<EventRaiserCounterChangedEventArgs> CounterChanged; public int Counter { get { return _counter; } set { if (_counter != value) { var old = _counter; _counter = value; OnCounterChanged(old, value); } } } public void DoWork() { new Thread(new ThreadStart(() => { for (var i = 0; i < 10; i++) Counter = i; })).Start(); } void OnCounterChanged(int oldValue, int newValue) { if (CounterChanged != null) CounterChanged.Invoke(this, new EventRaiserCounterChangedEventArgs(oldValue, newValue)); } } class EventRaiserCounterChangedEventArgs : EventArgs { public int NewValue { get; set; } public int OldValue { get; set; } public EventRaiserCounterChangedEventArgs(int oldValue, int newValue) { NewValue = newValue; OldValue = oldValue; } }
Tutaj mamy klasę z właściwością Counter, którą można zmienić z 0 na 10. W tym przypadku logika zmieniająca Counter jest przetwarzana w osobnym wątku.
A oto nasz punkt wyjścia:
class Program
{
static void Main(string[] args)
{
var raiser = new EventRaiser();
raiser.CounterChanged += Raiser_CounterChanged;
raiser.DoWork();
Console.ReadLine();
}
static void Raiser_CounterChanged(object sender, EventRaiserCounterChangedEventArgs e)
{
Console.WriteLine(string.Format("OldValue: {0}; NewValue: {1}", e.OldValue, e.NewValue));
}
}
Oznacza to, że tworzymy instancję naszego generatora, subskrybujemy zmianę licznika i, w obsłudze zdarzeń, wyprowadzamy wartości do konsoli.
Oto, co otrzymujemy w wyniku:
Jak na razie dobrze. Zastanówmy się jednak, w którym wątku wykonywany jest program obsługi zdarzeń?
Większość moich kolegów odpowiedziała na to pytanie „ogólnie jedno”. Oznaczało to, że żaden z nich nie rozumiał, jak układają się delegaci. Postaram się to wyjaśnić.
Klasa Delegate zawiera informacje o metodzie.
Istnieje również jego potomek, MulticastDelegate, który ma więcej niż jeden element.
Tak więc, gdy subskrybujesz wydarzenie, tworzone jest wystąpienie potomka MulticastDelegate. Każdy następny subskrybent dodaje nową metodę (obsługę zdarzeń) do już utworzonej instancji MulticastDelegate.
Po wywołaniu metody Invoke programy obsługi wszystkich subskrybentów są wywoływane jeden po drugim dla zdarzenia. Co więcej, wątek, w którym wywołujesz te programy obsługi, nie wie nic o wątku, w którym zostały określone, i odpowiednio nie może wstawić niczego do tego wątku.
Ogólnie procedury obsługi zdarzeń w powyższym przykładzie są wykonywane w wątku wygenerowanym w metodzie DoWork(). Czyli podczas generowania zdarzenia wątek, który je w ten sposób wygenerował, czeka na wykonanie wszystkich handlerów. Pokażę ci to bez wycofywania wątków identyfikatorów. W tym celu zmieniłem kilka linii kodu w powyższym przykładzie.
Dowód, że wszystkie procedury obsługi w powyższym przykładzie są wykonywane w wątku, który wywołał zdarzenie
Metoda generowania zdarzenia
void OnCounterChanged(int oldValue, int newValue) { if (CounterChanged != null) { CounterChanged.Invoke(this, new EventRaiserCounterChangedEventArgs(oldValue, newValue)); Console.WriteLine(string.Format("Event Raiser: old = {0}, new = {1}", oldValue, newValue)); } }
Obsługa
static void Raiser_CounterChanged(object sender, EventRaiserCounterChangedEventArgs e) { Console.WriteLine(string.Format("OldValue: {0}; NewValue: {1}", e.OldValue, e.NewValue)); Thread.Sleep(500); }
W module obsługi wysyłamy bieżący wątek do uśpienia na pół sekundy. Gdyby programy obsługi pracowały w głównym wątku, ten czas wystarczyłby, aby wątek wygenerowany w DoWork() zakończył swoje zadanie i wypisał swoje wyniki.
Jednak oto, co naprawdę widzimy:
Nie wiem, kto i jak powinien obsługiwać zdarzenia generowane przez klasę, którą napisałem, ale tak naprawdę nie chcę, aby te programy obsługi spowalniały pracę mojej klasy. Dlatego użyję metody BeginInvoke zamiast Invoke. BeginInvoke generuje nowy wątek.
Uwaga:Obie metody Invoke i BeginInvoke nie są członkami klas Delegate ani MulticastDelegate. Są członkami wygenerowanej klasy (lub opisanej powyżej klasy uniwersalnej).
Teraz, jeśli zmienimy metodę generowania zdarzenia, otrzymamy:
Generowanie zdarzeń wielowątkowych:
void OnCounterChanged(int oldValue, int newValue) { if (CounterChanged != null) { var delegates = CounterChanged.GetInvocationList(); for (var i = 0; i < delegates.Length; i++) ((EventHandler<EventRaiserCounterChangedEventArgs>)delegates[i]).BeginInvoke(this, new EventRaiserCounterChangedEventArgs(oldValue, newValue), null, null); Console.WriteLine(string.Format("Event Raiser: old = {0}, new = {1}", oldValue, newValue)); } }
Ostatnie dwa parametry są równe null. Pierwszy to callback, drugi to pewien parametr. W tym przykładzie nie używam wywołania zwrotnego, ponieważ jest to przykład pośredni. Może się przydać jako informacja zwrotna. Na przykład może pomóc klasie generującej zdarzenie w określeniu, czy zdarzenie zostało obsłużone i/lub czy jest wymagane uzyskanie wyników tej obsługi. Może również zwolnić zasoby związane z działaniem asynchronicznym.
Jeśli uruchomimy program, otrzymamy następujący wynik.
Myślę, że jest całkiem jasne, że teraz programy obsługi zdarzeń są wykonywane w osobnych wątkach, tj. generator zdarzeń nie dba o to, kto, jak i jak długo będzie obsługiwał swoje zdarzenia.
I tu pojawia się pytanie:co z obsługą sekwencyjną? W końcu mamy licznik. A jeśli byłaby to seryjna zmiana stanów? Ale nie odpowiem na to pytanie, nie jest to temat tego artykułu. Mogę tylko powiedzieć, że jest kilka sposobów.
I jeszcze jedno. Aby nie powtarzać w kółko tych samych czynności, sugeruję utworzenie dla nich osobnej klasy.
Klasa do generowania zdarzeń asynchronicznych
static class AsyncEventsHelper { public static void RaiseEventAsync<T>(EventHandler<T> h, object sender, T e) where T : EventArgs { if (h != null) { var delegates = h.GetInvocationList(); for (var i = 0; i < delegates.Length; i++) ((EventHandler<T>)delegates[i]).BeginInvoke(sender, e, h.EndInvoke, null); } } }
W tym przypadku korzystamy z wywołania zwrotnego. Jest wykonywany w tym samym wątku co handler. Oznacza to, że po zakończeniu metody obsługi delegat wywołuje następnie h.EndInvoke.
Oto jak należy go używać
void OnCounterChanged(int oldValue, int newValue) { AsyncEventsHelper.RaiseEventAsync(CounterChanged, this, new EventRaiserCounterChangedEventArgs(oldValue, newValue)); }
Myślę, że teraz jest jasne, dlaczego potrzebna była uniwersalna metoda. Jeśli opiszemy zdarzenia metodą 2, ta sztuczka nie zadziała. W przeciwnym razie będziesz musiał sam stworzyć uniwersalność dla swoich delegatów.
Uwaga :W przypadku realnych projektów polecam zmianę architektury wydarzeń w kontekście wątków. Opisane przykłady mogą zaszkodzić pracy aplikacji z wątkami i są podane wyłącznie w celach informacyjnych.
Wniosek
Mam nadzieję, że udało mi się opisać jak działają eventy i gdzie działają handlery. W następnym artykule zamierzam zagłębić się w uzyskiwanie wyników obsługi zdarzeń w przypadku wywołania asynchronicznego.
Czekam na Wasze komentarze i sugestie.