Race Conditions in Threading C#


Introduction

A race condition occurs when two or more threads are able to access shared data and they try to change it at the same time. Because the thread scheduling algorithm can swap between threads at any point, we cannot know the order at which the threads will attempt to access the shared data. Therefore, the result of the change in data is dependent on the thread scheduling algorithm, i.e. both threads are 'racing' to access/change the data. The reason for having more race conditions among threads than processes is that threads can share their memory while a process can not.

For example Let's say we have two threads, T1 and T2, where both are accessing a shared variable x.  They first read data from the variable and then try to write data on the variable simultaneously.  They would race to find which of these threads can write the value last to the variable.  In this case, the last written value would be saved.

 As another example, we can have these two threads, T1 and T2, trying opposite operations on this variable.  Let thread T1 increase the value of the variable and the thread T2 decrease the value of the variable at the same point of time.  What is the result?  The value remains unchanged.

Race Conditions

Consider the example shown below. We have a variable result and there are 3 threads trying to write a unique value to it

using System;
using System.Threading;
namespace
RaceCondition
    class Akshay
    {
        int result = 0;
        void Work1() { result = 1; }
        void Work2() { result = 2; }
        void Work3() { result = 3; }
        static void Main(string[] args)
        {
            Akshay a = new Akshay();
            Thread worker1 = new Thread(a.Work1);
            Thread worker2 = new Thread(a.Work2);
            Thread worker3 = new Thread(a.Work3);
            worker1.Start();
            worker2.Start();
            worker3.Start();
            Console.WriteLine(a.result);
            Console.Read();
        }
    }
}

output : Looking at the code it might seem that the result at the end will be 3. But if you try executing the code you will find out that it is not always true. The result might be 1, 2 ,3 or even 0 as

1.gif

2.gif

3.gif

The reason for this is that the operating system decides which thread gets executed first. And the order in which we start the threads is not all that important. So thread that is given least priority by the operating system gets executed last.

The Solution

C# provide number of ways to avoid race conditions depending on the type of application you are writing. But the most common method that works in any condition is using Wait Handles and Signaling. In the above example we will try to ensure that the first thread is the last one that writes value to result variable.

The process is quite simple let all threads are started at the same time. But the first thread is forced to wait till second and third thread signal that they are done.

Similarly the main thread is forced to wait till the first thread completes. This ensures that the  Console.WriteLine() is not called before the other threads finish their work.

using System;
using System.Threading;
namespace
RaceCondition
{
    class Akshay
    {
        int result = 0;
        AutoResetEvent event1 = new AutoResetEvent(false);
        AutoResetEvent event2 = new AutoResetEvent(false);
        AutoResetEvent event3 = new AutoResetEvent(false);
        void Work1()
        {
            WaitHandle.WaitAll(new WaitHandle[] { event2, event3 });
            result = 1;
            event1.Set();
        }
        void Work2() { result = 2; event2.Set(); }
        void Work3() { result = 3; event3.Set(); }
        static void Main(string[] args)
        {
            Akshay a = new Akshay();
            Thread worker1 = new Thread(a.Work1);
            Thread worker2 = new Thread(a.Work2);
            Thread worker3 = new Thread(a.Work3);
            WaitHandle[] waitHandles = new WaitHandle[] { a.event2, a.event3 };
            worker1.Start();
            worker2.Start();
            worker3.Start(); 
            WaitHandle.WaitAny(new WaitHandle[] { a.event1 });
            Console.WriteLine(a.result);
            Console.Read();
        }
    }
}

Output

 4.gif

To implement this we need to declare AutoResetEvent for each thread (except the main one). Whenever a thread finishes execution it signals its respective AutoResetEvent by calling Set(). The first thread waits for both second and third thread to signal and then starts execution.

Similarly the main thread waits for the first thread to signal before it prints the value. This method will always ensure that the final result is 1.