2

I am reading a book about C# and CLR and a can`t understand one thing. The text going below:

To fix this race condition, many developers write the OnNewMail method as follows:

// Version 2
protected virtual void OnNewMail(NewMailEventArgs e) {
    EventHandler<NewMailEventArgs> temp = NewMail;
    if (temp != null) temp(this, e);
}

The thinking here is that a reference to NewMail is copied into a temporary variable, temp, which refers to the chain of delegates at the moment the assignment is performed. Now, this method compares temp and null and invokes temp, so it doesn’t matter if another thread changes NewMail after the assignment to temp. Remember that delegates are immutable and this is why this technique works in theory.

So, the question is: why should it work? The temp and the NewMail objects are refer to the same object, i think, and no matters which has to be changed - the result will be affecting for both. Thanks!

  • It would be awesome if you could provide a [mcve]. Also please be explicit about the **name** and **page number** of the book. – mjwills Mar 24 '19 at 09:20
  • They refer to the same object but they are two separate variables (They are just two locations in memory with values that, at certain point in time, happen to be the same and both values are referencing the memory occupied by the original object). If you change the value of one variable to something different the other one still has the value (a reference) to the object – Steve Mar 24 '19 at 09:21
  • `The temp and the NewMail objects are refer to the same object` No, they are two **variables** pointing at the same **object**. If `NewMail` is assigned to a new object, then you will then have two **variables** pointing to two different **objects**. Assigning to one variable won't impact another variable. – mjwills Mar 24 '19 at 09:21
  • ok, i think i should to read more about subcribing. thank you. – Roman Alexeev Mar 24 '19 at 09:29
  • You need to understand better the concept of [value types and reference types](http://www.albahari.com/valuevsreftypes.aspx) – Steve Mar 24 '19 at 09:31

2 Answers2

1

The delegate types in CLR and MulticastDelegate type in particular, despite the fact that they are the reference types, belong to to the rare group of types called "immutable" 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. So when you write:

EventHandler<NewMailEventArgs> temp = NewMail;

the new copy of delegate referenced by NewMail is created and reference to this copy is assigned to the temp variable, so after execution of this line there will be two instances of the EventHandler<NewMailEventArgs> delegate: the instance referenced by NewMail and another instance referenced by temp (not a single instance referenced by two varaibles as you might think). That's why now you can safely call the delegate pointed by temp because it can't be null-ed by another thread during a period of time when the delegate is being invoked.

Dmytro Mukalov
  • 1,949
  • 1
  • 9
  • 14
1

A bulletproof way to avoid concurrency issues is using System.Threading.Volatile.Read method. Temporary variable can be removed with undocumented compiler optimizations. For now it works fine, but it can change in the future.

using System;
using System.Threading;

namespace EventHandling
{
    class Program
    {
        static void Main(string[] args)
        {
            var eventProvider = new EventProvider();
            eventProvider.Event += (sender, e) => Console.WriteLine("Event fired");
            eventProvider.FireEvent();
        }
    }

    class EventProvider
    {
        public event EventHandler Event;

        protected void OnEvent(EventArgs e) => Volatile.Read(ref Event)?.Invoke(this, e);

        public void FireEvent() => OnEvent(EventArgs.Empty);
    }
}

To explore how concurrency affects event handling one can try this code:

using System;
using System.Threading;
using System.Threading.Tasks;

namespace EventHandling
{
    class Program
    {
        static void Main(string[] args)
        {
            while(true)
                new Program().Run();
        }

        private void Run()
        {
            var eventProvider = new EventProvider();
            eventProvider.Event += HandleEvent;
            Console.WriteLine("subscribed");

            var unsubscribe = new Task(() =>
            {
                eventProvider.Event -= HandleEvent;
                Console.WriteLine("unsubscribed");
            });

            var fireEvent = new Task(() => eventProvider.FireEvent());

            fireEvent.Start();
            unsubscribe.Start();

            Task.WaitAll(fireEvent, unsubscribe);

            Console.ReadLine();
        }

        private void HandleEvent(object sender, EventArgs e) => Console.WriteLine("Event fired");


    }

    class EventProvider
    {
        public event EventHandler Event;

        protected void OnEvent(EventArgs e)
        {
            var temp = Volatile.Read(ref Event);
            Console.WriteLine("temp delegate created");
            Thread.Sleep(25); // time to unsubscribe concurrently

            if (temp != null)
            {
                Console.WriteLine("temp delegate invoking");
                temp.Invoke(this, e);
                Console.WriteLine("temp delegate invoked");
            }
            else
                Console.WriteLine("temp delegate is empty");
        }

        public void FireEvent() => OnEvent(EventArgs.Empty);
    }
}

Sometimes output is:

subscribed
unsubscribed
temp delegate created
temp delegate is empty

Sometimes:

subscribed
temp delegate created
unsubscribed
temp delegate invoking
Event fired
temp delegate invoked
CSDev
  • 3,177
  • 6
  • 19
  • 37
  • 1) `Volatile` is not bulletproof. A leading concurrency expert [claims that it is evil](http://joeduffyblog.com/2010/12/04/sayonara-volatile/). 2) The [null conditional operator](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/null-conditional-operators) that you use in your examples generates code that includes a temporary variable. In other words it uses the same approach to fix the race condition. – Theodor Zoulias Apr 06 '19 at 10:44
  • @Theodor Zoulias, thanks for the article. It's interesting. The temporary variable itself is not the thing. The point is Volatile.Read prevents it from being removed by compiler. The solution is suggested by Jeffrey Richter in his book "CLR via C#". – CSDev Apr 07 '19 at 09:08