События и потоки. Часть 1


Сразу скажу, что статья не про потоки, а про события в контексте потоков в .NET. Поэтому я не буду пытаться организовать работу потоков правильно (со всеми блокировками, колбэками, отменой и тд). Для правильной организации потоков есть другие статьи.

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

Во-первых хочу заметить, что для системы событий в .NET уже есть готовые делегаты, которые я очень советую вам использовать и не изобретать велосипедов. Например, я нередко встречал вот такие 2 способа организации событий:

Способ 1
    class WrongRaiser
    {
        public event Action 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 MyEvent;
    }

    class MyEventArgs : EventArgs
    {
        public object SomeProperty { get; set; }
    }

Как видите, здесь мы используем универсальный класс EventHandler. То есть определять собственный хандлер нет необходимости.

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

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

Посмотрим на простейший пример генератора событий

Пример
    class EventRaiser
    {
        int _counter;

        public event EventHandler 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));
        }
    }

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

Вот что мы получим в результате

ae921850e62f4e4dbfe827ed31b86dfd.PNG

Пока все ровно. Но подумайте, а в каком потоке выполняется обработчик события?

Задав этот вопрос коллегам, я получил ответ «в основном». Это означало, что никто из моих коллег не понимает как устроены делегаты. Я попытаюсь объяснить это на яблоках под спойлером, кто уже все знает, может не читать.

Устройство делегатов
Класс Delegate имеет информацию о методе.
Есть еще его наследник 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 () завершил свою работу и вывел свои результаты.
Но вот что мы видим на самом деле.

f4b4ccfcf65741fabef4577ecd409413.PNG

Я не знаю, кто и как будет обрабатывать события, порожденные написанным мной классом, но мне не очень то хочется, чтобы эти обработчики могли повесить работу моего класса. Поэтому я буду использовать метод 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)delegates[i]).BeginInvoke(this, new EventRaiserCounterChangedEventArgs(oldValue, newValue), null, null);
                Console.WriteLine(string.Format("Event Raiser: old = {0}, new = {1}", oldValue, newValue));
            }
                
        }

Два последних параметра у нас равны null. Первый — это колбэк, второй — некий параметр. Колбэк в данном примере нам не нужен. Он может пригодится для обратной связи, например, чтобы класс, генерирующий событие, мог узнать, было ли обработано событие и/или, если нужно, получить результаты этой обработки.

Если запустим программу, получим такой результат

dd9a7533faac4d66bff0bb052b3688ce.PNG

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

Тут возникает еще вопрос, а как же последовательная обработка? У нас же Counter. А что если это была бы последовательная смена состояний? Но ответ на этот вопрос я вам не дам, это не касается темы текущей статьи. Могу лишь сказать, что способов несколько.

Ну и еще. Чтобы не выполнять аналогичные действия раз за разом, предлагаю вынести их в отдельный класс.

Класс для генерации асинхронных событий
    static class AsyncEventsHelper
    {
        public static void RaiseEventAsync(EventHandler h, object sender, T e) where T: EventArgs
        {
            if (h != null)
            {
                var delegates = h.GetInvocationList();
                for (var i = 0; i < delegates.Length; i++)
                    ((EventHandler)delegates[i]).BeginInvoke(sender, e, null, null);
            }
        }
    }

Используем его вот так

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

Думаю, теперь стало понятно (если не было), зачем нужен был универсальный способ. Если описывать события способом 2, то такая штука не прокатит. Ну или вам придется самостоятельно создавать универсальность для ваших делегатов.

Заключение

Надеюсь, я смог донести информацию о том, как работают события и где работают обработчики. В следующей части я планирую углубиться и рассказать, как можно получить результаты обработки событий при асинхронном вызове.

Жду комментариев с предложениями, дополнениями, вопросами.

Комментарии (0)

© Habrahabr.ru