2

Thread-safe Events in C#

 2 years ago
source link: https://www.codeproject.com/Articles/5327025/Thread-safe-Events-in-Csharp
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

3 most common ways to check for null-value and raise an Even

In articles on Internet, you will find a lot of discussions on what is the best and thread-safe way to check for null-value and raise Event in C#. Usually there are 3 methods mentioned and discussed:

Copy Code
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 analysis of each of them

Analyzing Method A

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

Analyzing Method B

Key to understanding this approach is to really well understand what is happening in (B1). We there have objects and assignment between them.

At first, one might think, we have 2 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 of 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 compiler is EventHandler<EventArgs>. So, we basically have assignment of an Event to a delegate. That is same as if we wrote

Copy Code
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 copy of an instance unlike the assignment of regular reference types which just copies the values of references. Key thing here is what really happens with InvocationList (that is of type Delegate[]) which contains list of all added delegates. What is seems is that list is Cloned in that assignment. That is key reason why Method B will work, because nobody other has access to newly created variable TmpEvent and its inner InvocationList of type Delegate[].

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

Analyzing Method C

This method is based on null-conditional operator that is available from C#6. For thread safety, we need to trust Microsoft and it’s 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.”

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

Race-thread attack on 3 proposed approaches

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

Here is demo code:

Shrink ▲   Copy 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 execution result:

Image 1

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

B) In order to attack Method B, we at (B11) launch 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 this attack. Key thing is 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 2 EventHandlers (C11) and will during execution of the first one, attack with racing thread (C12) and try to remove the second handler. We will from printouts see that attack has failed and both EventHandlers were executed. Interesting is to look at 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 preferred way to check for null-value and raise an Even.

References

[1] https://stackoverflow.com/questions/55322255/what-if-i-will-copy-a-reference-to-an-event-object-to-another-object-and-will-ch

[2] https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/member-access-operators#null-conditional-operators--and-


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK