Written by 16:19 Database development, Events

Events and Threads in .NET

I’d like to tell you straight off that this article will concern not threads in particular, but events in the context of threads in .NET.  So, I won’t try arranging threads correctly (with all blocks, callbacks, canceling, etc.) There are many articles on this subject.

All examples are written in C# for the framework version 4.0 (in 4.6, everything is slightly easier, but still, there are many projects in 4.0). I will also try to stick to C# version 5.0.

Firstly, I’d like to note that there are ready delegates for the .Net event system that I highly recommend to use instead of inventing something new. For example, I frequently faced the following 2 methods for organizing events.

First method:

 class WrongRaiser
    {
        public event Action<object> MyEvent;
        public event Action MyEvent2;
    }

I would recommend using this method carefully. If you don’t universalize it, you may eventually write more code than expected. As such, it won’t set a more precise structure if compared with the methods below.

From my experience, I can tell that I used it when I began working with events and consequently made a fool of myself. Now, I would never make it happen.

Second method:

    class WrongRaiser
    {
        public event MyDelegate MyEvent;
    }

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

    delegate void MyDelegate(object sender, MyEventArgs e);

This method is quite valid, but it is good for specific cases when the method below does not work for some reasons. Otherwise, you may get lots of monotonous work.

And now, let’s take a look at what has been already created for the events.

Universal method:

    class Raiser
    {
        public event EventHandler<MyEventArgs> MyEvent;
    }

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

As you can see, here we use the universal EventHandler class. That is, there is no need in defining your own handler.

The further examples feature the universal method.

Let’s take a look at the simplest example of the events generator.

    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;
        }
    }

Here we have a class with the Counter property that can be changed from 0 to 10. At that, the logic that changes Counter is processed in a separate thread.

And here is our entry point:

    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));
        }
    }

That is, we create an instance of our generator, subscribe to the counter change and, in the event handler, output values to console.

Here is what we get as a result:

So far, so good. But let us think, in which thread is event handler executed?

Most of my colleagues answered this question “In general one”. It meant that none of them did not understand how delegates are arranged. I will try to explain it.

The Delegate class contains information about a method.

There is also its descendant, MulticastDelegate, that has more than one element.

So, when you subscribe to an event, an instance of the MulticastDelegate descendant is created. Each next subscriber adds a new method (event handler) into the already created instance of MulticastDelegate.

When you call the Invoke method, the handlers of all subscribers are called one by one for your event. At that, the thread in which you call these handlers does not know a thing about the thread in which they were specified and, correspondingly, it cannot insert anything into that thread.

In general, the event handlers in the example above are executed in the thread generated in the DoWork() method. That is, during event generation, the thread that generated it in such a way, is waiting for execution of all handlers. I will show you this without withdrawing Id threads. For this, I changed few code lines in the above example.

Proof that all the handlers in the above example are executed in the thread that called the event

Method where event is generated

        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));
            }
                
        }

Handler

        static void Raiser_CounterChanged(object sender, EventRaiserCounterChangedEventArgs e)
        {
            Console.WriteLine(string.Format("OldValue: {0}; NewValue: {1}", e.OldValue, e.NewValue));
            Thread.Sleep(500);
        }

In the handler, we send the current thread to sleep for a half a second. If handlers worked in the main thread, this time would be enough for a thread generated in DoWork() to finish its job and output its results.

However, here is what we really see:

I don’t know who and how should handle the events generated by the class I wrote, but I don’t really want these handlers to slow down the work of my class. That is why, I will use the BeginInvoke method instead of Invoke. BeginInvoke generates a new thread.

Note: Both, the Invoke and BeginInvoke methods are not the members of the Delegate or MulticastDelegate classes. They are the members of the generated class (or the universal class described above).

Now, if we change the method in which the event is generated, we will get the following:

Multi-threaded events generation:

        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));
            }
                
        }

The last two parameters equal null. The first one is a callback, the second one is a certain parameter. I do not use callback in this example, since the example is intermediate. It may be useful for feedback. For instance, it can help the class that generates the event to determine, whether an event was handled and/or if it is required to get results of this handling. It also can free resources related to asynchronous operation.

If we run the program, we will get the following result.

I guess, it is quite clear that now event handlers are executed in separate threads, i.e. the event generator does not care of who, how and how long will handle its events.

And here the question arises: what about sequential handling? We have Counter, after all. What if it would be a serial change of states? But I won’t answer this question, it is not a subject of this article. I can only say that there are several ways.

And one more thing. In order to not repeat the same actions over and over again, I suggest creating a separate class for them.

A class for generation of asynchronous events

    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);
            }
        }
    }

In this case, we use callback. It is executed in the same thread as the handler. That is, after the handler method is completed, the delegate calls h.EndInvoke next.

Here is how is should be used

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

I guess, that it is now clear why the universal method was needed. If we describe events with method 2, this trick won’t work. Otherwise, you will have to create universality for your delegates on your own.

Note: For real projects, I recommend changing the events architecture in the context of threads. The described examples may afflict damage to the work of application with threads, and are provided for informative purposes only.

Conclusion

Hope, I managed to describe how events work and where handlers work. In the next article, I’m planning to dive deep into getting results of the event handling when an asynchronous call is made.

I look forward to your comments and suggestions.

Tags: , Last modified: September 23, 2021
Close