Ich möchte Ihnen gleich sagen, dass es in diesem Artikel nicht speziell um Threads geht, sondern um Ereignisse im Zusammenhang mit Threads in .NET. Daher werde ich nicht versuchen, Threads richtig anzuordnen (mit allen Blockierungen, Rückrufen, Abbrüchen usw.). Es gibt viele Artikel zu diesem Thema.
Alle Beispiele sind in C# für die Framework-Version 4.0 geschrieben (in 4.6 ist alles etwas einfacher, aber trotzdem gibt es viele Projekte in 4.0). Ich werde auch versuchen, mich an die C#-Version 5.0 zu halten.
Zunächst möchte ich anmerken, dass es fertige Delegierte für das .Net-Ereignissystem gibt, deren Verwendung ich dringend empfehle, anstatt etwas Neues zu erfinden. Zum Beispiel bin ich häufig mit den folgenden 2 Methoden zur Organisation von Veranstaltungen konfrontiert worden.
Erste Methode:
class WrongRaiser { public event Action<object> MyEvent; public event Action MyEvent2; }
Ich würde empfehlen, diese Methode sorgfältig anzuwenden. Wenn Sie es nicht verallgemeinern, schreiben Sie möglicherweise mehr Code als erwartet. Daher wird im Vergleich zu den folgenden Methoden keine genauere Struktur festgelegt.
Aus meiner Erfahrung kann ich sagen, dass ich es benutzt habe, als ich anfing, mit Ereignissen zu arbeiten, und mich folglich lächerlich gemacht habe. Nun, ich würde es niemals geschehen lassen.
Zweite Methode:
class WrongRaiser { public event MyDelegate MyEvent; } class MyEventArgs { public object SomeProperty { get; set; } } delegate void MyDelegate(object sender, MyEventArgs e);
Diese Methode ist ziemlich gültig, aber sie ist gut für bestimmte Fälle, in denen die folgende Methode aus bestimmten Gründen nicht funktioniert. Andernfalls könnten Sie viel monotone Arbeit bekommen.
Sehen wir uns nun an, was bereits für die Events erstellt wurde.
Universelle Methode:
class Raiser { public event EventHandler<MyEventArgs> MyEvent; } class MyEventArgs : EventArgs { public object SomeProperty { get; set; } }
Wie Sie sehen können, verwenden wir hier die universelle EventHandler-Klasse. Das heißt, es besteht keine Notwendigkeit, einen eigenen Handler zu definieren.
Die weiteren Beispiele zeigen die universelle Methode.
Schauen wir uns das einfachste Beispiel des Ereignisgenerators an.
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; } }
Hier haben wir eine Klasse mit der Counter-Eigenschaft, die von 0 auf 10 geändert werden kann. Dabei wird die Logik, die Counter ändert, in einem separaten Thread verarbeitet.
Und hier ist unser Einstiegspunkt:
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));
}
}
Das heißt, wir erstellen eine Instanz unseres Generators, abonnieren die Zähleränderung und geben im Ereignishandler Werte an die Konsole aus.
Folgendes erhalten wir als Ergebnis:
So weit, ist es gut. Aber denken wir mal darüber nach, in welchem Thread der Eventhandler ausgeführt wird?
Die meisten meiner Kollegen beantworteten diese Frage mit „Allgemein“. Es bedeutete, dass keiner von ihnen nicht verstand, wie Delegierte angeordnet sind. Ich werde versuchen, es zu erklären.
Die Delegate-Klasse enthält Informationen über eine Methode.
Es gibt auch seinen Nachkommen, MulticastDelegate, der mehr als ein Element hat.
Wenn Sie also ein Ereignis abonnieren, wird eine Instanz des MulticastDelegate-Nachkommen erstellt. Jeder nächste Abonnent fügt der bereits erstellten Instanz von MulticastDelegate eine neue Methode (Event-Handler) hinzu.
Wenn Sie die Invoke-Methode aufrufen, werden die Handler aller Abonnenten nacheinander für Ihr Ereignis aufgerufen. Dabei weiß der Thread, in dem Sie diese Handler aufrufen, nichts über den Thread, in dem sie angegeben wurden, und kann dementsprechend nichts in diesen Thread einfügen.
Im Allgemeinen werden die Event-Handler im obigen Beispiel in dem Thread ausgeführt, der in der DoWork()-Methode generiert wird. Das heißt, während der Ereignisgenerierung wartet der Thread, der es auf diese Weise generiert hat, auf die Ausführung aller Handler. Ich werde Ihnen dies zeigen, ohne Id-Threads zurückzuziehen. Dafür habe ich im obigen Beispiel einige Codezeilen geändert.
Beweisen Sie, dass alle Handler im obigen Beispiel in dem Thread ausgeführt werden, der das Ereignis aufgerufen hat
Methode, bei der das Ereignis generiert wird
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)); } }
Handler
static void Raiser_CounterChanged(object sender, EventRaiserCounterChangedEventArgs e) { Console.WriteLine(string.Format("OldValue: {0}; NewValue: {1}", e.OldValue, e.NewValue)); Thread.Sleep(500); }
Im Handler versetzen wir den aktuellen Thread für eine halbe Sekunde in den Ruhezustand. Wenn Handler im Haupt-Thread arbeiten würden, würde diese Zeit für einen in DoWork() generierten Thread ausreichen, um seine Arbeit zu beenden und seine Ergebnisse auszugeben.
Hier ist jedoch, was wir wirklich sehen:
Ich weiß nicht, wer und wie die von der von mir geschriebenen Klasse generierten Ereignisse behandeln soll, aber ich möchte nicht wirklich, dass diese Handler die Arbeit meiner Klasse verlangsamen. Aus diesem Grund werde ich die BeginInvoke-Methode anstelle von Invoke verwenden. BeginInvoke generiert einen neuen Thread.
Hinweis:Sowohl die Methoden Invoke als auch BeginInvoke sind keine Mitglieder der Klassen Delegate oder MulticastDelegate. Sie sind die Mitglieder der generierten Klasse (oder der oben beschriebenen universellen Klasse).
Wenn wir nun die Methode ändern, in der das Ereignis generiert wird, erhalten wir Folgendes:
Generierung von Multithread-Ereignissen:
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)); } }
Die letzten beiden Parameter sind gleich null. Der erste ist ein Callback, der zweite ein bestimmter Parameter. Ich verwende in diesem Beispiel keinen Rückruf, da das Beispiel mittelschwer ist. Es kann für Feedback nützlich sein. Beispielsweise kann es der Klasse helfen, die das Ereignis generiert, um festzustellen, ob ein Ereignis behandelt wurde und/oder ob es erforderlich ist, Ergebnisse dieser Behandlung zu erhalten. Es kann auch Ressourcen im Zusammenhang mit asynchronen Vorgängen freigeben.
Wenn wir das Programm ausführen, erhalten wir das folgende Ergebnis.
Ich denke, es ist ziemlich klar, dass jetzt Event-Handler in separaten Threads ausgeführt werden, d.h. der Event-Generator kümmert sich nicht darum, wer, wie und wie lange seine Events behandelt.
Und hier stellt sich die Frage:Wie sieht es mit der sequentiellen Handhabung aus? Immerhin haben wir Counter. Was wäre, wenn es sich um einen seriellen Zustandswechsel handeln würde? Aber ich werde diese Frage nicht beantworten, sie ist nicht Thema dieses Artikels. Ich kann nur sagen, dass es mehrere Möglichkeiten gibt.
Und noch etwas. Um dieselben Aktionen nicht immer wieder zu wiederholen, schlage ich vor, eine separate Klasse dafür zu erstellen.
Eine Klasse zur Generierung von asynchronen Ereignissen
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); } } }
In diesem Fall verwenden wir Callback. Es wird im selben Thread wie der Handler ausgeführt. Das heißt, nachdem die Behandlungsmethode abgeschlossen ist, ruft der Delegat als nächstes h.EndInvoke auf.
So sollte es verwendet werden
void OnCounterChanged(int oldValue, int newValue) { AsyncEventsHelper.RaiseEventAsync(CounterChanged, this, new EventRaiserCounterChangedEventArgs(oldValue, newValue)); }
Ich denke, dass jetzt klar ist, warum die universelle Methode benötigt wurde. Wenn wir Ereignisse mit Methode 2 beschreiben, funktioniert dieser Trick nicht. Andernfalls müssen Sie selbst Universalität für Ihre Delegierten schaffen.
Hinweis :Für echte Projekte empfehle ich, die Ereignisarchitektur im Kontext von Threads zu ändern. Die beschriebenen Beispiele können die Arbeit der Anwendung mit Threads beeinträchtigen und dienen nur zu Informationszwecken.
Schlussfolgerung
Hope, ich habe es geschafft zu beschreiben, wie Events funktionieren und wo Handler arbeiten. Im nächsten Artikel plane ich, tief in die Ergebnisse der Ereignisbehandlung einzutauchen, wenn ein asynchroner Aufruf erfolgt.
Ich freue mich auf Ihre Kommentare und Anregungen.