Бих искал да ви кажа направо, че тази статия не се отнася конкретно за нишки, а за събития в контекста на нишки в .NET. Така че няма да се опитвам да подредя правилно нишките (с всички блокове, обратни извиквания, анулиране и т.н.) Има много статии по тази тема.
Всички примери са написани на C# за рамката версия 4.0 (в 4.6 всичко е малко по-лесно, но все пак има много проекти в 4.0). Също така ще се опитам да се придържам към C# версия 5.0.
Първо, бих искал да отбележа, че има готови делегати за системата за събития .Net, които силно препоръчвам да използвате, вместо да измисляте нещо ново. Например, често се сблъсквах със следните 2 метода за организиране на събития.
Първи метод:
class WrongRaiser { public event Action<object> MyEvent; public event Action MyEvent2; }
Бих препоръчал да използвате този метод внимателно. Ако не го универсализирате, в крайна сметка може да напишете повече код от очакваното. Като такъв, той няма да зададе по-прецизна структура в сравнение с методите по-долу.
От моя опит мога да кажа, че го използвах, когато започнах да работя със събития и следователно направих глупак. Сега никога не бих го направил.
Втори метод:
class WrongRaiser { public event MyDelegate MyEvent; } class MyEventArgs { public object SomeProperty { get; set; } } delegate void MyDelegate(object sender, MyEventArgs e);
Този метод е доста валиден, но е добър за конкретни случаи, когато методът по-долу не работи по някакви причини. В противен случай може да получите много монотонна работа.
И сега, нека да разгледаме какво вече е създадено за събитията.
Универсален метод:
class Raiser { public event EventHandler<MyEventArgs> MyEvent; } class MyEventArgs : EventArgs { public object SomeProperty { get; set; } }
Както можете да видите, тук използваме универсалния клас EventHandler. Тоест, няма нужда да дефинирате свой собствен манипулатор.
Следващите примери включват универсалния метод.
Нека да разгледаме най-простия пример за генератора на събития.
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; } }
Тук имаме клас със свойството Counter, което може да бъде променено от 0 на 10. При това логиката, която променя Counter, се обработва в отделна нишка.
А ето и нашата входна точка:
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));
}
}
Тоест създаваме екземпляр на нашия генератор, абонираме се за промяната на брояча и в манипулатора на събития извеждаме стойности в конзолата.
Ето какво получаваме в резултат:
Дотук добре. Но нека помислим, в коя нишка се изпълнява манипулаторът на събития?
Повечето от моите колеги отговориха на този въпрос „Общо взето“. Това означаваше, че никой от тях не разбира как са подредени делегатите. Ще се опитам да го обясня.
Класът Delegate съдържа информация за метод.
Има и негов наследник, MulticastDelegate, който има повече от един елемент.
Така че, когато се абонирате за събитие, се създава екземпляр на наследника MulticastDelegate. Всеки следващ абонат добавя нов метод (манипулатор на събития) към вече създадения екземпляр на MulticastDelegate.
Когато извикате метода Invoke, манипулаторите на всички абонати се извикват един по един за вашето събитие. При това нишката, в която извиквате тези манипулатори, не знае нищо за нишката, в която са посочени и съответно не може да вмъкне нищо в тази нишка.
Като цяло, манипулаторите на събития в примера по-горе се изпълняват в нишката, генерирана в метода DoWork(). Тоест, по време на генериране на събитие, нишката, която го е генерирала по такъв начин, чака за изпълнение на всички манипулатори. Ще ви покажа това, без да изтеглям идентификационните нишки. За това промених няколко реда код в горния пример.
Доказателство, че всички манипулатори в горния пример се изпълняват в нишката, която е извикала събитието
Метод, при който се генерира събитие
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)); } }
Обработчик
static void Raiser_CounterChanged(object sender, EventRaiserCounterChangedEventArgs e) { Console.WriteLine(string.Format("OldValue: {0}; NewValue: {1}", e.OldValue, e.NewValue)); Thread.Sleep(500); }
В манипулатора изпращаме текущата нишка да заспи за половин секунда. Ако манипулаторите работеха в основната нишка, това време би било достатъчно за нишка, генерирана в DoWork(), да завърши работата си и да изведе резултатите си.
Но ето какво наистина виждаме:
Не знам кой и как трябва да обработва събитията, генерирани от класа, който написах, но всъщност не искам тези манипулатори да забавят работата на моя клас. Ето защо ще използвам метода BeginInvoke вместо Invoke. BeginInvoke генерира нова нишка.
Забележка:И двата метода Invoke и BeginInvoke не са членове на класовете Delegate или MulticastDelegate. Те са членовете на генерирания клас (или на универсалния клас, описан по-горе).
Сега, ако променим метода, по който се генерира събитието, ще получим следното:
Генериране на многонишкови събития:
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)); } }
Последните два параметъра са равни на нула. Първият е обратно извикване, вторият е определен параметър. Не използвам обратно извикване в този пример, тъй като примерът е междинен. Може да е полезно за обратна връзка. Например, може да помогне на класа, който генерира събитието, да определи дали дадено събитие е било обработено и/или дали е необходимо да получи резултати от тази обработка. Освен това може да освободи ресурси, свързани с асинхронна работа.
Ако стартираме програмата, ще получим следния резултат.
Предполагам, че е съвсем ясно, че сега манипулаторите на събития се изпълняват в отделни нишки, т.е. генераторът на събития не се интересува кой, как и колко време ще обработва събитията му.
И тук възниква въпросът:какво да кажем за последователната обработка? Все пак имаме Counter. Ами ако това ще бъде последователна промяна на състоянията? Но няма да отговарям на този въпрос, той не е тема на тази статия. Мога само да кажа, че има няколко начина.
И още нещо. За да не се повтарят едни и същи действия отново и отново, предлагам да създадете отделен клас за тях.
Клас за генериране на асинхронни събития
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); } } }
В този случай използваме обратно извикване. Изпълнява се в същата нишка като манипулатора. Тоест, след като методът манипулатор е завършен, делегатът извиква h.EndInvoke next.
Ето как трябва да се използва
void OnCounterChanged(int oldValue, int newValue) { AsyncEventsHelper.RaiseEventAsync(CounterChanged, this, new EventRaiserCounterChangedEventArgs(oldValue, newValue)); }
Предполагам, че вече е ясно защо беше необходим универсалният метод. Ако опишем събития с метод 2, този трик няма да работи. В противен случай ще трябва да създадете универсалност за вашите делегати сами.
Забележка :За реални проекти препоръчвам промяна на архитектурата на събитията в контекста на нишките. Описаните примери могат да повредят работата на приложението с нишки и са предоставени само за информационни цели.
Заключение
Надявам се, успях да опиша как работят събитията и къде работят манипулаторите. В следващата статия планирам да се потопя дълбоко в получаването на резултати от обработката на събития, когато се извърши асинхронно повикване.
Очаквам вашите коментари и предложения.