Jag skulle vilja berätta direkt att den här artikeln inte kommer att beröra trådar i synnerhet, utan händelser i samband med trådar i .NET. Så jag kommer inte att försöka ordna trådar korrekt (med alla blockeringar, återuppringningar, avbokningar, etc.) Det finns många artiklar om detta ämne.
Alla exempel är skrivna i C# för ramverksversion 4.0 (i 4.6 är allt något enklare, men ändå finns det många projekt i 4.0). Jag kommer också att försöka hålla mig till C# version 5.0.
För det första vill jag notera att det finns redo delegater för .Net-evenemangssystemet som jag starkt rekommenderar att använda istället för att uppfinna något nytt. Till exempel stötte jag ofta på följande två metoder för att organisera evenemang.
Första metoden:
class WrongRaiser { public event Action<object> MyEvent; public event Action MyEvent2; }
Jag skulle rekommendera att använda denna metod noggrant. Om du inte universaliserar det kan du så småningom skriva mer kod än förväntat. Som sådan kommer den inte att ställa in en mer exakt struktur om den jämförs med metoderna nedan.
Av min erfarenhet kan jag säga att jag använde det när jag började arbeta med händelser och följaktligen gjorde mig narr. Nu skulle jag aldrig få det att hända.
Andra metoden:
class WrongRaiser { public event MyDelegate MyEvent; } class MyEventArgs { public object SomeProperty { get; set; } } delegate void MyDelegate(object sender, MyEventArgs e);
Denna metod är ganska giltig, men den är bra för specifika fall när metoden nedan inte fungerar av någon anledning. Annars kan du få massor av monotont arbete.
Och nu, låt oss ta en titt på vad som redan har skapats för evenemangen.
Universell metod:
class Raiser { public event EventHandler<MyEventArgs> MyEvent; } class MyEventArgs : EventArgs { public object SomeProperty { get; set; } }
Som du kan se använder vi här den universella EventHandler-klassen. Det vill säga, det finns inget behov av att definiera din egen hanterare.
De ytterligare exemplen visar den universella metoden.
Låt oss ta en titt på det enklaste exemplet på händelsegeneratorn.
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; } }
Här har vi en klass med Counter-egenskapen som kan ändras från 0 till 10. Då bearbetas logiken som ändrar Counter i en separat tråd.
Och här är vår ingångspunkt:
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));
}
}
Det vill säga, vi skapar en instans av vår generator, prenumererar på räknarändringen och, i händelsehanteraren, matar ut värden till konsolen.
Här är vad vi får som resultat:
Än så länge är allt bra. Men låt oss fundera, i vilken tråd körs händelsehanteraren?
De flesta av mina kollegor svarade på denna fråga "I allmänhet en". Det gjorde att ingen av dem inte förstod hur delegater är ordnade. Jag ska försöka förklara det.
Klassen Delegate innehåller information om en metod.
Det finns också dess ättling, MulticastDelegate, som har mer än ett element.
Så när du prenumererar på ett evenemang skapas en instans av MulticastDelegate-avkomlingen. Varje nästa prenumerant lägger till en ny metod (händelsehanterare) i den redan skapade instansen av MulticastDelegate.
När du anropar Invoke-metoden, anropas hanterarna för alla prenumeranter en efter en för din händelse. Då vet inte tråden där du kallar dessa hanterare någonting om tråden där de specificerades och på motsvarande sätt kan den inte infoga något i den tråden.
I allmänhet exekveras händelsehanterarna i exemplet ovan i tråden som genereras i DoWork()-metoden. Det vill säga, under händelsegenerering väntar tråden som genererade den på ett sådant sätt på exekvering av alla hanterare. Jag kommer att visa dig detta utan att dra tillbaka ID-trådar. För detta ändrade jag några kodrader i exemplet ovan.
Bevis på att alla hanterare i exemplet ovan exekveras i tråden som kallade händelsen
Metod där händelse genereras
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)); } }
Hanterare
static void Raiser_CounterChanged(object sender, EventRaiserCounterChangedEventArgs e) { Console.WriteLine(string.Format("OldValue: {0}; NewValue: {1}", e.OldValue, e.NewValue)); Thread.Sleep(500); }
I hanteraren skickar vi den aktuella tråden att sova i en halv sekund. Om hanterare fungerade i huvudtråden skulle den här gången räcka för att en tråd genererad i DoWork() skulle slutföra sitt jobb och mata ut sina resultat.
Men här är vad vi verkligen ser:
Jag vet inte vem och hur som ska hantera händelserna som genereras av klassen jag skrev, men jag vill inte riktigt att dessa hanterare ska sakta ner arbetet i min klass. Det är därför jag kommer att använda BeginInvoke-metoden istället för Invoke. BeginInvoke genererar en ny tråd.
Obs:Båda metoderna Invoke och BeginInvoke är inte medlemmar i klasserna Delegate eller MulticastDelegate. De är medlemmar av den genererade klassen (eller den universella klassen som beskrivs ovan).
Om vi nu ändrar metoden som händelsen genereras i får vi följande:
Generering av flertrådiga händelser:
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)); } }
De två sista parametrarna är lika med null. Den första är en återuppringning, den andra är en viss parameter. Jag använder inte callback i det här exemplet, eftersom exemplet är mellanliggande. Det kan vara användbart för feedback. Det kan till exempel hjälpa klassen som genererar händelsen att avgöra om en händelse hanterades och/eller om det krävs för att få resultat av denna hantering. Det kan också frigöra resurser relaterade till asynkron drift.
Om vi kör programmet får vi följande resultat.
Jag antar att det är ganska tydligt att nu händelsehanterare exekveras i separata trådar, dvs händelsegeneratorn bryr sig inte om vem, hur och hur länge som kommer att hantera sina händelser.
Och här uppstår frågan:hur är det med sekventiell hantering? Vi har trots allt Counter. Tänk om det skulle vara ett seriebyte av stater? Men jag kommer inte att svara på den här frågan, den är inte ett ämne för den här artikeln. Jag kan bara säga att det finns flera sätt.
Och en sak till. För att inte upprepa samma åtgärder om och om igen föreslår jag att du skapar en separat klass för dem.
En klass för generering av asynkrona händelser
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); } } }
I det här fallet använder vi återuppringning. Den körs i samma tråd som hanteraren. Det vill säga, efter att hanterarmetoden är klar, anropar delegaten h.EndInvoke next.
Här är hur den ska användas
void OnCounterChanged(int oldValue, int newValue) { AsyncEventsHelper.RaiseEventAsync(CounterChanged, this, new EventRaiserCounterChangedEventArgs(oldValue, newValue)); }
Jag antar att det nu är klart varför den universella metoden behövdes. Om vi beskriver händelser med metod 2 kommer det här tricket inte att fungera. Annars måste du skapa universalitet för dina delegater på egen hand.
Obs :För riktiga projekt rekommenderar jag att du ändrar evenemangsarkitekturen i samband med trådar. De beskrivna exemplen kan skada applikationsarbetet med trådar och tillhandahålls endast i informativa syften.
Slutsats
Hope, jag lyckades beskriva hur event fungerar och var hanterare jobbar. I nästa artikel planerar jag att djupdyka i att få resultat av händelsehanteringen när ett asynkront samtal görs.
Jag ser fram emot dina kommentarer och förslag.