Rád bych vám rovnou řekl, že tento článek se nebude týkat konkrétních vláken, ale událostí v kontextu vláken v .NET. Nebudu se tedy pokoušet správně uspořádat vlákna (se všemi bloky, zpětnými voláními, rušením atd.). Na toto téma je mnoho článků.
Všechny příklady jsou napsány v C# pro framework verze 4.0 (ve 4.6 je vše o něco jednodušší, ale přesto je ve 4.0 mnoho projektů). Také se pokusím držet C# verze 5.0.
Nejprve bych rád poznamenal, že pro systém událostí .Net jsou připraveni delegáti, které vřele doporučuji používat místo vymýšlení něčeho nového. Často jsem se například potýkal s následujícími 2 způsoby organizace akcí.
První metoda:
class WrongRaiser { public event Action<object> MyEvent; public event Action MyEvent2; }
Doporučuji používat tuto metodu opatrně. Pokud to neuskutečníte, můžete nakonec napsat více kódu, než se očekávalo. Jako takový nenastaví přesnější strukturu ve srovnání s metodami níže.
Ze své zkušenosti mohu říci, že jsem to použil, když jsem začal pracovat s událostmi a následně ze sebe udělal blázna. Teď bych to nikdy nedokázal.
Druhý způsob:
class WrongRaiser { public event MyDelegate MyEvent; } class MyEventArgs { public object SomeProperty { get; set; } } delegate void MyDelegate(object sender, MyEventArgs e);
Tato metoda je docela validní, ale je dobrá pro specifické případy, kdy níže uvedená metoda z nějakých důvodů nefunguje. Jinak můžete mít spoustu monotónní práce.
A nyní se podívejme na to, co již bylo pro události vytvořeno.
Univerzální metoda:
class Raiser { public event EventHandler<MyEventArgs> MyEvent; } class MyEventArgs : EventArgs { public object SomeProperty { get; set; } }
Jak vidíte, zde používáme univerzální třídu EventHandler. To znamená, že není potřeba definovat svůj vlastní handler.
Další příklady představují univerzální metodu.
Podívejme se na nejjednodušší příklad generátoru událostí.
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; } }
Zde máme třídu s vlastností Counter, kterou lze změnit od 0 do 10. Logika, která mění Counter, je přitom zpracovávána v samostatném vláknu.
A zde je náš vstupní bod:
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));
}
}
To znamená, že vytvoříme instanci našeho generátoru, přihlásíme se k odběru změn počítadla a v obslužné rutině události odešleme hodnoty do konzole.
Výsledkem je toto:
Zatím je vše dobré. Ale zamysleme se nad tím, ve kterém vlákně se provádí obsluha události?
Většina mých kolegů na tuto otázku odpověděla „obecně“. Znamenalo to, že nikdo z nich nechápal, jak jsou delegáti uspořádáni. Pokusím se to vysvětlit.
Třída Delegát obsahuje informace o metodě.
Existuje také jeho potomek, MulticastDelegate, který má více než jeden prvek.
Když se tedy přihlásíte k odběru události, vytvoří se instance potomka MulticastDelegate. Každý další odběratel přidá novou metodu (obslužnou rutinu události) do již vytvořené instance MulticastDelegate.
Když zavoláte metodu Invoke, budou pro vaši událost jeden po druhém volány obslužné rutiny všech účastníků. Vlákno, ve kterém tyto ovladače voláte, přitom neví nic o vláknu, ve kterém byly specifikovány, a tudíž do tohoto vlákna nemůže nic vložit.
Obecně platí, že obslužné rutiny událostí ve výše uvedeném příkladu se spouštějí ve vláknu generovaném metodou DoWork(). To znamená, že během generování události vlákno, které ji tímto způsobem vygenerovalo, čeká na provedení všech handlerů. Ukážu vám to, aniž bych stahoval vlákna ID. Za tímto účelem jsem ve výše uvedeném příkladu změnil několik řádků kódu.
Důkaz, že všechny obslužné rutiny ve výše uvedeném příkladu jsou spuštěny ve vláknu, které volalo událost
Metoda generování události
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)); } }
Obslužný nástroj
static void Raiser_CounterChanged(object sender, EventRaiserCounterChangedEventArgs e) { Console.WriteLine(string.Format("OldValue: {0}; NewValue: {1}", e.OldValue, e.NewValue)); Thread.Sleep(500); }
V handleru odešleme aktuální vlákno na půl sekundy do spánku. Pokud by handlery pracovaly v hlavním vláknu, tato doba by stačila na to, aby vlákno vygenerované v DoWork() dokončilo svou práci a vydalo své výsledky.
Zde je však to, co skutečně vidíme:
Nevím, kdo a jak by měl zpracovávat události generované třídou, kterou jsem napsal, ale opravdu nechci, aby tyto handlery zpomalovaly práci mé třídy. Proto místo Invoke použiji metodu BeginInvoke. BeginInvoke vygeneruje nové vlákno.
Poznámka:Obě metody Invoke a BeginInvoke nejsou členy tříd Delegát nebo MulticastDelegate. Jsou to členové vygenerované třídy (nebo výše popsané univerzální třídy).
Pokud nyní změníme metodu, kterou je událost generována, dostaneme následující:
Generování vícevláknových událostí:
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)); } }
Poslední dva parametry se rovnají nule. Prvním je zpětné volání, druhým je určitý parametr. V tomto příkladu nepoužívám zpětné volání, protože příklad je střední. Může to být užitečné pro zpětnou vazbu. Například může pomoci třídě, která generuje událost, určit, zda byla událost zpracována a/nebo zda je potřeba získat výsledky tohoto zpracování. Může také uvolnit zdroje související s asynchronním provozem.
Pokud program spustíme, dostaneme následující výsledek.
Myslím, že je zcela jasné, že nyní jsou obslužné rutiny událostí prováděny v samostatných vláknech, tj. generátor událostí se nestará o to, kdo, jak a jak dlouho bude zpracovávat jeho události.
A zde vyvstává otázka:co sekvenční zpracování? Koneckonců máme Counter. Co když by to byla sériová změna stavů? Na tuto otázku ale neodpovím, není předmětem tohoto článku. Mohu pouze říci, že existuje několik způsobů.
A ještě jedna věc. Aby se stejné akce neopakovaly znovu a znovu, navrhuji pro ně vytvořit samostatnou třídu.
Třída pro generování asynchronních událostí
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); } } }
V tomto případě používáme zpětné volání. Provádí se ve stejném vlákně jako handler. To znamená, že po dokončení metody handler delegát zavolá h.EndInvoke next.
Zde je návod, jak by se měl používat
void OnCounterChanged(int oldValue, int newValue) { AsyncEventsHelper.RaiseEventAsync(CounterChanged, this, new EventRaiserCounterChangedEventArgs(oldValue, newValue)); }
Myslím, že nyní je jasné, proč byla potřeba univerzální metoda. Pokud popíšeme události metodou 2, tento trik nebude fungovat. V opačném případě budete muset vytvořit univerzálnost pro své delegáty sami.
Poznámka :U skutečných projektů doporučuji změnit architekturu událostí v kontextu vláken. Popsané příklady mohou poškodit práci aplikace s vlákny a jsou poskytovány pouze pro informativní účely.
Závěr
Doufám, že se mi podařilo popsat, jak akce fungují a kde pracují handleři. V příštím článku se hodlám ponořit hluboko do získávání výsledků zpracování událostí při asynchronním volání.
Těším se na vaše komentáře a návrhy.