'Is the `is` operator thread-safe/atomic in C#?

Is the following code thread-safe?

public object DemoObject {get;set;}

public void DemoMethod()
{
    if (DemoObject is IDemoInterface demo)
    {
        demo.DoSomething();
    }
}

If other threads modify DemoObject (e.g. set to null) while DemoMethod is being processed, is it guaranteed that within the if block the local variable demo will always be assigned correctly (to an instance of type IDemoInterface)?



Solution 1:[1]

The is construct here is atomic much like interlocked. However the behavior of this code is almost 100% non deterministic. Unless the objective is to create unpredictable and non deterministic behavior this would be a bug.

Valid usage example of this code: In a game to simulate the possibility of some non deterministic event such as "Neo from the Matrix catching a bullet in mid air", this method may be more non deterministic that simply using a pseudo random number generator.

In any scenario where deterministic / predictable behavior is expected this code is a bug.

Explanation:

if (DemoObject is IDemoInterface demo)

is evaluated and assigned pseudo atomically.

Thereafter within the if statement: even if DemoObject is set to null by another thread the value of demo has already been assigned and the DoSomething() operation is executed on the already assigned instance.

To answer your comment questions:

why is there a race?

The race condition is by design in this code. In the example code below:

  1. 16 threads are competing to set the value of DemoObject to null
  2. while another 16 threads are competing to set the value of DemoObject to an instance of DemoClass.
  3. At the same time 16 threads are competing to execute DoSomething() whenever they win the race condition when DemoObject is NOT null.

See: What is a race condition?

and why can i not predict whether DoSomething() will execute?

DoSomething() will execute each time

if (DemoObject is IDemoInterface demo)

evaluates to true. Each time DemoObject is null or NOT IDemoInterface it will NOT execute. You cannot predict when it will execute. You can only predict that it will execute whenever the thread executing DoSomething() manages to get a reference to a non null instance of DemoObject. Or in other words when a thread running DemoMethod() manages to win the race condition:

A) after a thread running DemoMethod_Assign() wins the race condition

B) and before a thread running DemoMethod_Null() wins the race condition

Caveat: As per my understanding (Someone else please clarify this point) DemoObject may be both null and not null at the same time across different threads.

DemoObject may be read from cache or may be read from main memory. We cannot make it volatile since it is an object reference. Therefore the state of DemoObject may be simultaneously Null for one thread and not null for another thread. Meaning its value is non deterministic. In Schrödinger's cat, the cat is both dead and alive simultaneously. We have much the same situation here.

There are no locks or memory barriers in this code with respect to DemoObject. However a thread context switch forces the equivalent of a memory barrier. Therefore any thread resuming after a context switch will have an accurate copy of the value of DemoObject as retrieved from main memory. However a different thread may have altered the value of DemoObject but this altered value may not have been flushed to main memory yet. Which then brings into question which is the actual accurate value? The value fetched from main memory or the value not yet flushed to main memory.

Note: Someone else please clarify this Caveat as I may have missed something.

Here is some code to validate everything above except the Caveat. Ran this console app test on a machine with 64 logical cores. Null reference exception is never thrown.

internal class Program
{
    private static ManualResetEvent BenchWaitHandle = new ManualResetEvent(false);

    private class DemoClass : IDemoInterface
    {
        public void DoSomething()
        {
            Interlocked.Increment(ref Program.DidSomethingCount);
        }
    }
    private interface IDemoInterface
    {
        void DoSomething();
    }
    private static object DemoObject { get; set; }
    public static volatile int DidSomethingCount = 0;

    private static void DemoMethod()
    {
        BenchWaitHandle.WaitOne();
        for (int i = 0; i < 100000000; i++)
        {
            try
            {
                if (DemoObject is IDemoInterface demo)
                {
                    demo.DoSomething();
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.ToString());
            }
        }
    }
    private static bool m_IsRunning = false;
    private static object RunningLock = new object();
    private static bool IsRunning 
    { 
        get { lock (RunningLock) { return m_IsRunning; } } 
        set { lock(RunningLock) { m_IsRunning = value; } }
    }
    private static void DemoMethod_Assign()
    {
        BenchWaitHandle.WaitOne();
        while (IsRunning)
        {
            DemoObject = new DemoClass();
        }
    }
    private static void DemoMethod_Null()
    {
        BenchWaitHandle.WaitOne();
        while (IsRunning)
        {
            DemoObject = null;
        }
    }
    static void Main(string[] args)
    {
        List<Thread> threadsListDoWork = new List<Thread>();
        List<Thread> threadsList = new List<Thread>();
        BenchWaitHandle.Reset();
        for (int I =0; I < 16; I++)
        {
            threadsListDoWork.Add(new Thread(new ThreadStart(DemoMethod)));
            threadsList.Add(new Thread(new ThreadStart(DemoMethod_Assign)));
            threadsList.Add(new Thread(new ThreadStart(DemoMethod_Null)));
        }
        foreach (Thread t in threadsListDoWork)
        {
            t.Start();
        }
        foreach (Thread t in threadsList)
        {
            t.Start();
        }
        IsRunning = true;
        BenchWaitHandle.Set();
        foreach (Thread t in threadsListDoWork)
        {
            t.Join();
        }
        IsRunning = false;
        foreach (Thread t in threadsList)
        {
            t.Join();
        }

        Console.WriteLine(@"Did Something {0} times", DidSomethingCount);
        Console.ReadLine();
}
//On the last run this printed
//Did Something 112780926 times
//Which means that DemoMethod() threads won the race condition slightly over 7% of the time.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1