Події і потоки. Частина 1

Відразу скажу, що стаття не про потоки, а про події в контексті потоків .NET. Тому я не буду намагатися організувати роботу потоків правильно (з усіма блокуваннями, колбэками, скасуванням і тд). Для правильної організації потоків є інші статті.

Всі приклади будуть написані на мові C# для версії фреймворку 4.0 (на 4.6 все трохи простіше, але все ще є багато проектів на 4.0). Так само буду намагатися дотримуватися версії C# 5.0.


По-перше хочу зауважити, що для системи подій .NET вже є готові делегати, які я дуже раджу вам використовувати і не винаходити велосипеда. Наприклад, я часто зустрічав ось такі 2 способи організації подій:

Спосіб 1
class WrongRaiser
{
public event Action<object> MyEvent;
public event Action MyEvent2;
}

Якщо ви робите так, відмовтеся від цього. Якщо ви не робите додаток з 10 рядків коду з мінімумом логіки і навантаження, то в кінцевому рахунку це може створити проблеми.


Спосіб 2
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. Тобто визначати власний хандлер немає необхідності.


Є ще спосіб — створити новий клас, успадкований від Delegate або MulticastDelegate, але це вже для зовсім специфічних випадків. Фактично, спосіб 2 це і робить за лаштунками.

Для подальших прикладів буде використаний універсальний спосіб.

Подивимося на найпростіший приклад генератора подій

Приклад
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));
}
}


Тобто створюємо екземпляр нашого генератора, підписуємося на зміну каунтера і в оброблювачі події виводимо на консоль значення.

Ось що ми отримаємо в результаті




Поки все рівно. Але подумайте, а в якому потоці виконується обробник події?

Задавши це питання колегам, я отримав відповідь «в основному». Це означало, що ніхто з моїх колег не розуміє як влаштовані делегати. Я спробую пояснити це на яблуках під спойлером, хто вже все знає, може не читати.

Пристрій делегатівКлас Представник має інформацію про метод.
Є ще його спадкоємець MulticastDelegate, який має більше одного елемента.

Так от, коли ви підписуєтеся на подію, створюється екземпляр спадкоємця від MulticastDelegate. Кожен наступний передплатник додає у вже створений екземпляр MulticastDelegate новий метод (обробник події).

І коли ви викликаєте метод Invoke, для вашої події, починають викликатися обробники всіх передплатників по черзі. При цьому потік, в якому ви викликаєте ці обробники нічого не знає про потоці, в якому вони були задані і відповідно не може в цей потік нічого підсунути.


Загалом оброблювачі події в прикладі вище виконуються в потоці, породженому в методі DoWork(). Тобто при генерації події, потік, який його створив таким чином, чекає виконання всіх обробників. Я доведу це без висмикування Id потоків, просто логічно. Для цього я змінив кілька місць у прикладі вище

Доказ того, що всі обробники в прикладі вище виконуються в потоці, що викликала подіяМетод, де у нас породжується подія
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));
}

}


Два останніх параметра у нас рівні null. Перший — це колбек, другий — якийсь параметр. Колбек в даному прикладі нам не потрібен. Він може стане в нагоді для зворотного зв'язку, наприклад, щоб клас, генерує подію, міг дізнатися, чи було опрацьовано подія та/або, якщо потрібно, отримати результати цієї обробки.

Якщо запустимо програму, отримаємо такий результат




Думаю, всім зрозуміло, що тепер обробники подій виконуються в окремих потоках. Тобто генератора подій тепер до лампочки, хто, як і як довго буде обробляти його події.

Тут виникає ще питання, а як же послідовна обробка? У нас же 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, null, null);
}
}
}


Використовуємо його ось так

void OnCounterChanged(int oldValue, int newValue)
{
AsyncEventsHelper.RaiseEventAsync(CounterChanged, this, new EventRaiserCounterChangedEventArgs(oldValue, newValue)); 
}


Думаю, тепер стало зрозуміло (якщо не було), навіщо був потрібен універсальний спосіб. Якщо описувати події способом 2, то така штука не прокотить. Ну або вам доведеться самостійно створювати універсальність для ваших делегатів.


Висновок

Сподіваюся, я зміг донести інформацію про те, як працюють події і де працюють обробники. У наступній частині я планую заглибитися і розповісти, як можна отримати результати обробки подій при асинхронному виклик.

Чекаю коментарів з пропозиціями, доповненнями, питаннями.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.