I’ll say right away that the article is not about threads, but about events in the context of threads in .NET. Therefore, I will not try to organize the work of streams correctly (with all the locks, callbacks, cancellations, etc.). For the proper organization of the flow, there are other articles.
All examples will be written in C # for version 4.0 of the framework (at 4.6 everything is somewhat simpler, but there are still many projects at 4.0). I will also try to stick with C # 5.0.
First, I want to note that there are already ready delegates for the event system in .NET, which I strongly advise you to use and not reinvent the wheel. For example, I often met these 2 ways of organizing events:
Method 1class WrongRaiser { public event Action<object> MyEvent; public event Action MyEvent2; }
I would advise using this method with caution. If it is not universalized, then you can write as a result more code than you could, which, in doing so, will not define a clearer structure than in the case of the methods below.
From my own experience I can say that I started working with events in this way and eventually ended up in a trouble. Now I would not allow this, but other methods have already become a habit.
Method 2 class WrongRaiser { public event MyDelegate MyEvent; } class MyEventArgs { public object SomeProperty { get; set; } } delegate void MyDelegate(object sender, MyEventArgs e);
This method has the right to life, but it should be used for special cases, when for some reason the method described below does not fit. Otherwise, you can get a lot of monotonous work.
Now that has already been created for events.
')
Universal way class Raiser { public event EventHandler<MyEventArgs> MyEvent; } class MyEventArgs : EventArgs { public object SomeProperty { get; set; } }
As you can see, here we use the generic EventHandler class. That is, there is no need to define your own handler.
For further examples, a universal method will be used.
Let's look at the simplest example of an event generator.
Example 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; } }
We have a class that has the Counter property and can change it from 0 to 10. Moreover, the logic that changes the 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 a change to a counter and change the values in the event handler to the console.
This is what we get as a result.

While everything is smooth. But think, and in what flow the event handler is executed?
After asking this question to my colleagues, I received the answer “mostly”. This meant that none of my colleagues understood how delegates work. I will try to explain it on apples under the spoiler, who already knows everything, can not read.
Device delegatesThe Delegate class has information about the method.
There is also his heir MulticastDelegate, which has more than one element.
So, when you subscribe to an event, an instance of the heir from MulticastDelegate is created. Each next subscriber adds a new method (event handler) to the already created instance of MulticastDelegate.
And when you call the Invoke method, for your event, handlers of all subscribers follow up in turn. In this case, the thread in which you call these handlers does not know anything about the thread in which they were specified and, accordingly, cannot slip anything into this thread.
In general, the event handlers in the example above are executed in the stream spawned in the DoWork () method. That is, when generating an event, the thread that generated it in this way waits for all handlers to be executed. I will prove it without pulling out Id streams, just logically. For this, I changed a couple of places in the example above.
Proof that all the handlers in the example above are executed in the thread that caused the eventThe method where we generate the event:
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 put the current stream to sleep for half a second. If the handlers worked in the main thread, this time would be enough for the thread generated in DoWork () to complete its work and output its results.
But that's what we see in reality.

I do not know who and how will handle the events generated by the class I wrote, but I don’t really want these handlers to hang the work of my class. Therefore, I will use the BeginInvoke method instead of Invoke. BeginInvoke spawns a new thread.
NoteBoth the Invoke method and the BeginInvoke method are not members of the Delegate or MulticastDelegate class, they are members of the generated class (or for the universal class of the class already described).
Now, changing the method in which the event is generated, we get the following
Multithread event 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 are null. The first is a callback, the second is a parameter. I do not use the callback in this example, because the example is intermediate. It can be useful for feedback, for example, so that the class generating the event can find out whether the event was processed and / or, if necessary, get the results of this processing, and, as suggested in the comments, frees the resources associated with the asynchronous operation. .
If we run the program, we get this result.

I think everyone understands that now event handlers are executed in separate threads. That is, the event generator is now up to the bulb, who, how and for how long will process its events.
This raises another question: what about sequential processing? We have the same Counter. And what if it would be a successive change of states? But the answer to this question I will not give you, it does not concern the topic of the current article. I can only say that there are several ways.
Well, and more. In order not to perform similar actions time after time, I propose to bring them into a separate class.
Class for generating 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 has completed, the delegate calls h.EndInvoke.
Use it like this
void OnCounterChanged(int oldValue, int newValue) { AsyncEventsHelper.RaiseEventAsync(CounterChanged, this, new EventRaiserCounterChangedEventArgs(oldValue, newValue)); }
I think it has now become clear (if not) why a universal way was needed. If you describe events in method 2, then such a thing does not work. Well, or you have to create your own versatility for your delegates.
UPDATE:For real projects, I advise you to change the architecture of events in the context of threads. The described examples can harm the work of the application with threads and are presented only for the experiment and as an introduction.
Conclusion
Hopefully, I was able to convey information about how events work and where handlers work. In the next part, I plan to go deep and tell you how to get the results of event handling during an asynchronous call.
I am waiting for comments with suggestions, additions, questions.