Thread-Safe Events In C#

In this article, we discuss the three most common ways to check for null-value and raise Events in C#. Thread safety is analyzed as well. Then, in a small demo program creating a thread race situation, we attack each solution and demo its thread-safety.

The three most common ways to check for null-value and raise an Event

In articles on Internet, you will find many discussions about what the best and most thread-safe way to check for null-value and raise Event in C# is. Usually, there are three methods that are most often mentioned and discussed:

public static event EventHandler < EventArgs > MyEvent;
Object obj1 = new Object();
EventArgs args1 = new EventArgs();
//Method A
if (MyEvent != null) //(A1)
{
    MyEvent(obj1, args1); //(A2)
}
//Method B
var TmpEvent = MyEvent; //(B1)
if (TmpEvent != null) //(B2)
{
    TmpEvent(obj1, args1); //(B3)
}
//Method C
MyEvent?.Invoke(obj1, args1); //(C1)

Let us immediately give an answer: Method A is not thread-safe, while Methods B and C are thread-safe ways to check for null-value and raise an Event. Let us provide an analysis of each of these methods.

Analyzing Method A

In order to avoid NullReferenceException, in (A1) we check for null, then in (A2) we raise the Event. The problem is that in the time between (A1) and (A2), some other thread can access Event MyEvent and change its status. So, this approach is not thread safe. We demo that in our code (below), where we successfully launch a race-thread attack on this approach.

Analyzing Method B

The key to understanding this approach is to truly understand what is happening in (B1). There we have objects and assignment between them.

At first, one might think, we have two C# object references and assignment between them, So, they should be pointing to the same C# object. That is not the case here, since then there would be no point in that assignment. Events are C# objects (you can assign Object obj=MyEvent, and that is legal), but that assignment in (B1) is different there.

The real type of TmpEvent generated by the compiler is EventHandler<EventArgs>. So, basically, we have an assignment of an Event to a delegate. If we assume that Events and Delegates are different types (see text below), conceptually compiler is doing implicit cast, which is same as if we wrote:

//not needed, just a concept of what compiler is implicitly doing
EventHandler<EventArgs> TmpEvent = EventA as EventHandler<EventArgs>;   //(**)

As explained in [1], Delegates are immutable reference types. This implies that the reference assignment operation for such types creates a copy of an instance unlike the assignment of regular reference types, which just copies the values of references. The key thing here is what actually happens with InvocationList (that is of type Delegate[]), which contains a list of all added delegates. It seems is that the list is Cloned in that assignment. That is the key reason why Method B will work, because no one else has access to the newly created variable TmpEvent and its inner InvocationList of type Delegate[].

We demo that this approach is thread safe in our code (below), where we launch race-thread attack on this approach.

Analyzing Method C

This method is based on the null-conditional operator that is available from C#6. For thread safety, we need to trust Microsoft and its documentation. In [2] they say “The ‘?.’ operator evaluates its left-hand operand no more than once, guaranteeing that it cannot be changed to null after being verified as non-null…. Use the ?. operator to check if a delegate is non-null and invoke it in a thread-safe way (for example, when you raise an event).”

We demo that this approach is thread safe in our code (below) where we launch race-thread attack on this approach.

Are Events the same as Delegates?

In the above text at (**) we were arguing that in (B1) we have an implicit cast from Event to a Delegate. But are Events and Delegates the same or different types in C#?

If you look at [3] you will find author Jon Skeet strongly arguing that Events and Delegates are not the same. To quote: "Events aren't delegate instances. It's unfortunate in some ways that C# lets you use them in the same way in certain situations, but it's very important that you understand the difference. I find the easiest way to understand events is to think of them a bit like properties. While properties look like they're fields, they're definitely not…Events are pairs of methods, appropriately decorated in IL to tie them together…”

So, based on the text above by Jon Skeet, and comments on the article below by Paulo Zemek [4], we can accept the interpretation that “events are like special kind of properties.” Following on that analogy, we can in our demo program below replace:

public static event EventHandler<EventArgs> EventA;
public static event EventHandler<EventArgs> EventB;
public static event EventHandler<EventArgs> EventC;

with

public static EventHandler<EventArgs> EventA { get; set; } = null;
public static EventHandler<EventArgs> EventB { get; set; } = null;
public static EventHandler<EventArgs> EventC { get; set; } = null;

and everything will still work. Also, it is interesting to try this code:

public static event EventHandler<EventArgs> EventD1;
public static EventHandler<EventArgs> EventD2 { get; set; } = null;
public static EventHandler<EventArgs> EventD3;
EventD1 = EventD2 = EventD3 = delegate { };
Console.WriteLine("Type of EventD1: {0}", EventD1.GetType().Name);
Console.WriteLine("Type of EventD2: {0}", EventD2.GetType().Name);
Console.WriteLine("Type of EventD3: {0}", EventD3.GetType().Name);

You will get this response:

Type of EventD1: EventHandler`1
Type of EventD2: EventHandler`1
Type of EventD3: EventHandler`1

Back to reality, events are created by the “event” keyword, and therefore they are separate constructs in the C# language from properties or delegates. We can “interpret” them to say that they are “alike” properties or delegates, but they are not the same. The truth is, Events are whatever the compiler is labeling with that keyword “event”, and it seems that it makes them look like C# Delegates.

I am inclined to think this: Events and Delegates are strictly speaking not the same, but in C# language it seems that they are treated interchangeably, in a very similar manner, so it has become custom in the industry to talk about them as they are the same. Even in Microsoft documentation [2], the author uses the terms Event and Delegate interchangeably when discussing the null-conditional operator “?.”. In one moment author talks about “..raise an event," then the next sentence says “…delegate instances are immutable…”, etc.

Race-thread attack on three proposed approaches

In order to verify the thread safety of three proposed approaches, we created a small demo program. This program is not a definite answer for all cases and cannot be considered a “proof”, but it can still show/demo some interesting points. In order to set up race situations, we slow down threads with some Thread.Sleep() calls.

Here is the demo code:

internal class Client {
    public static event EventHandler < EventArgs > EventA;
    public static event EventHandler < EventArgs > EventB;
    public static event EventHandler < EventArgs > EventC;
    public static void HandlerA1(object obj, EventArgs args1) {
        Console.WriteLine("ThreadId:{0}, HandlerA1 invoked", Thread.CurrentThread.ManagedThreadId);
    }
    public static void HandlerB1(object obj, EventArgs args1) {
        Console.WriteLine("ThreadId:{0}, HandlerB1 invoked", Thread.CurrentThread.ManagedThreadId);
    }
    public static void HandlerC1(object obj, EventArgs args1) {
        Console.WriteLine("ThreadId:{0}, HandlerC1 - Start", Thread.CurrentThread.ManagedThreadId);
        Thread.Sleep(3000);
        Console.WriteLine("ThreadId:{0}, HandlerC1 - End", Thread.CurrentThread.ManagedThreadId);
    }
    public static void HandlerC2(object obj, EventArgs args1) {
        Console.WriteLine("ThreadId:{0}, HandlerC2 invoked", Thread.CurrentThread.ManagedThreadId);
    }
    static void Main(string[] args) {
        // Demo Method A for firing of Event-------------------------------
        Console.WriteLine("Demo A =========================");
        EventA += HandlerA1;
        Task.Factory.StartNew(() => //(A11)
            {
                Thread.Sleep(1000);
                Console.WriteLine("ThreadId:{0}, About to remove handler HandlerA1", Thread.CurrentThread.ManagedThreadId);
                EventA -= HandlerA1;
                Console.WriteLine("ThreadId:{0}, Removed handler HandlerA1", Thread.CurrentThread.ManagedThreadId);
            });
        if (EventA != null) {
            Console.WriteLine("ThreadId:{0}, EventA is null:{1}", Thread.CurrentThread.ManagedThreadId, EventA == null);
            Thread.Sleep(2000);
            Console.WriteLine("ThreadId:{0}, EventA is null:{1}", Thread.CurrentThread.ManagedThreadId, EventA == null);
            Object obj1 = new Object();
            EventArgs args1 = new EventArgs();
            try {
                EventA(obj1, args1); //(A12)
            } catch (Exception ex) {
                Console.WriteLine("ThreadId:{0}, Exception:{1}", Thread.CurrentThread.ManagedThreadId, ex.Message);
            }
        }
        // Demo Method B for firing of Event-------------------------------
        Console.WriteLine("Demo B =========================");
        EventB += HandlerB1;
        Task.Factory.StartNew(() => //(B11)
            {
                Thread.Sleep(1000);
                Console.WriteLine("ThreadId:{0}, About to remove handler HandlerB1", Thread.CurrentThread.ManagedThreadId);
                EventB -= HandlerB1;
                Console.WriteLine("ThreadId:{0}, Removed handler HandlerB1", Thread.CurrentThread.ManagedThreadId);
            });
        var TmpEvent = EventB;
        if (TmpEvent != null) {
            Console.WriteLine("ThreadId:{0}, EventB is null:{1}", Thread.CurrentThread.ManagedThreadId, EventB == null);
            Console.WriteLine("ThreadId:{0}, TmpEvent is null:{1}", Thread.CurrentThread.ManagedThreadId, TmpEvent == null);
            Thread.Sleep(2000);
            Console.WriteLine("ThreadId:{0}, EventB is null:{1}", //(B13)
                Thread.CurrentThread.ManagedThreadId, EventB == null);
            Console.WriteLine("ThreadId:{0}, TmpEvent is null:{1}", //(B14)
                Thread.CurrentThread.ManagedThreadId, TmpEvent == null);
            Object obj1 = new Object();
            EventArgs args1 = new EventArgs();
            try {
                TmpEvent(obj1, args1); //(B12)
            } catch (Exception ex) {
                Console.WriteLine("ThreadId:{0}, Exception:{1}", Thread.CurrentThread.ManagedThreadId, ex.Message);
            }
        }
        // Demo Method C for firing of Event-------------------------------
        Console.WriteLine("Demo C =========================");
        EventC += HandlerC1;
        EventC += HandlerC2; //(C11)
        Task.Factory.StartNew(() => //(C12)
            {
                Thread.Sleep(1000);
                Console.WriteLine("ThreadId:{0}, About to remove handler HandlerC2", Thread.CurrentThread.ManagedThreadId);
                EventC -= HandlerC2;
                Console.WriteLine("ThreadId:{0}, Removed handler HandlerC2", Thread.CurrentThread.ManagedThreadId);
            });
        Console.WriteLine("ThreadId:{0}, EventC has EventHandlers:{1}", Thread.CurrentThread.ManagedThreadId, EventC?.GetInvocationList().Length);
        try {
            Object obj1 = new Object();
            EventArgs args1 = new EventArgs();
            EventC?.Invoke(obj1, args1);
            Console.WriteLine("ThreadId:{0}, EventC has EventHandlers:{1}", Thread.CurrentThread.ManagedThreadId, EventC?.GetInvocationList().Length); //(C13)
        } catch (Exception ex) {
            Console.WriteLine("ThreadId:{0}, Exception:{1}", Thread.CurrentThread.ManagedThreadId, ex.Message);
        }
        Console.WriteLine("End =========================");
        Console.ReadLine();
    }
}

And here is the execution result:

A) In order to attack Method A, we at (A11) launched a new racing thread that is going to do some damage. We will see that it succeeds to create NullReferenceException at (A12).

B) In order to attack Method B, we at (B11) launched a new racing thread that is going to do some damage. We will see that at (B12) nothing eventful will happen and this approach will survive the attack. The key thing is the printout at (B13) and (B14) that will show that TmpEvent is not affected by changes to EventB.

C) We will attack method C in a different way. We know that EventHandlers are invoked synchronously. We will create two EventHandlers (C11) and will, during the execution of the first one, attack with racing thread (C12) and try to remove the second handler. We will, from printouts, see that this attack has failed and both EventHandlers were executed. Interesting is to look at the output at (C13) that shows that AFTER EventC reports decreased number of EventHandlers.

Conclusion

The best solution is to avoid thread-racing situations and to access Events from a single thread. But, if you need, Method C based on null-conditional operator is the preferred way to check for null-value and raise an Event.

References


Similar Articles