Pull to refresh

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

Reading time6 min
Views23K
Сразу скажу, что статья не про потоки, а про события в контексте потоков в .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;
    }

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

Способ 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. То есть определять собственный хандлер нет необходимости.


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

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

Пример
    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(). То есть при генерации события, поток, который его сгенерировал таким образом, дожидается выполнения всех обработчиков. Я докажу это без выдергивания 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, h.EndInvoke, null);
            }
        }
    }

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

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

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


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

UPDATE:
Для реальных проектов советую изменить архитектуру событий в контексте потоков. Описанные примеры могут навредить работе приложения с потоками и представлены только для эксперимента и в качестве ознакомления.

Заключение


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

Жду комментариев с предложениями, дополнениями, вопросами.
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+10
Comments94

Articles