Atomic Methods Thread Safety and Race Conditions in C#

Atomic Methods, Thread Safety, and Race Conditions in C#

In this article, I am going to discuss Atomic Methods, Thread Safety, and Race Conditions in C# with Examples. Please read our previous article, where we discussed How to Cancel Parallel Operations in C# with Examples.

Atomic Methods in C#:

So far, the Parallel Methods (For, Foreach and Invoke) that we have to invoke are completely self-sufficient. In the sense that they don’t need external data to work. But this is not always going to be the case. Sometimes we will want to share data between threads. An important concept to take into account is the concept of Atomic Methods in C#. Atomic Methods can be used comfortably in a multithreaded environment because they guarantee determinism, that is we will always obtain the same result, no matter how many threads try to execute the method simultaneously.

Characteristics of Atomic Methods in C#:

There are two fundamental characteristics of Atomic Methods in C#.

  1. First, if one thread is executing an atomic method, then another thread cannot see an intermediate state that is the operation has either not started or has already been completed. But there is no intermediate state between beginning and end.
  2. Second, the operation will be completed successfully or will fail completely without making any modifications. This part is similar to database transactions where either all operations are successful or none are performed if there is at least one error.
How to Achieve Atomicity in C#?

There are several ways to achieve Atomicity in C#. The most common way is to use locks. Locks allow us to block other threads from executing a piece of code when the lock is activated. If we are working with collections, then another option is to use concurrent collections, which are specially designed to handle multithreaded scenarios. If we don’t use proper mechanisms to have automaticity in our methods or operations, then we will end up with unexpected results, corrupted data, or incorrect values.

Thread Safety in C#:

An important concept in a parallelism environment is thread-safe. When we say that a method is thread-safe, we are saying that we can execute this method simultaneously from multiple threads without causing any kind of error. We know that we have thread safety when the application data is not corrupted if two or more threads try to perform operations on the same data at the same time.

How to Achieve Thread Safety in C#?

What do we have to do to have a thread-safe method in C#? Well, it all depends on what we do within the method. If within the method We added an external variable. Then we could have a problem with unexpected results in that variable. Something that we can use to mitigate this is to use a synchronization mechanism like using Interlocked or using locks.

If we need to transform objects, then we can use immutable objects to avoid problems of corrupting those objects.

Ideally, we should work with pure functions. Pure functions are those that return the same value for the same arguments and do not cause secondary effects.

Race Conditions in C#:

Race conditions occur in C# when we have a variable shared by several threads and these threads want to modify the variables simultaneously. The problem with this is that depending on the order of the sequence of operations done on a variable by different threads, the value of the variable will be different. Operations are simple as increasing by one.

A variable is problematic if we do them in multithreaded scenarios on a shared variable. The reason is that even increasing by 1 a variable or adding 1 to the variable is problematic. This is because the operation is not Atomic. A simple variable increment is not an atomic operation.

In fact, it is divided into three parts reading, increasing, and writing. Given the fact that we have three operations, two threads can execute them in such a way that even if we increase the value of a variable twice, only one increase takes effect.

Example to Understand Race Conditions in C#:

For example, in the following table, what happens if two threads sequentially try to increment a variable. We have Thread 1 in column one and Thread 2 in column 2. And in the end, a value column represents the value of the variable. For a better understanding, please have a look at the below diagram.

Example to Understand Race Conditions in C#

Initially, the value of the variable is zero. Thread 1 with the variable and then it has its value 0 in memory. Then Thread 1 increments that value again in memory and finally it provides that value into the variable. And then the value of the variable is 1. For a better understanding, please have a look at the below diagram.

Atomic Methods, Thread Safety, and Race Conditions in C#

Then after that thread 2 reads the variable value which has now the value 1, it increments the value in memory. And finally, it writes back to the variable. And the value of the variable now is 2. For a better understanding, please have a look at the below diagram.

Atomic Methods, Thread Safety, and Race Conditions in C#

This is as expected. However, what can happen if the two threads try to update the variable simultaneously?

What happens if two threads try to update the variable simultaneously?

Well, the result could be that the final value of the variable is either 1 or 2. Let’s say one possibility. Please have a look at the below diagram. Here again, we have Thread 1, Thread 2, and the value of the variable.

What happens if two threads try to update the variable simultaneously?

Now, Thread 1 and Thread 2 both read the values and so they both have the value of zero in memory. For a better understanding, please have a look at the below image.

What happens if two threads try to update the variable simultaneously?

Third 1 increment the value, as well as Thread 2, also increment the value and both of them increment it to 1 in memory. For a better understanding, please have a look at the below image.

What happens if two threads try to update the variable simultaneously?

Once both the threads increment the value to 1 in memory. Then Thread 1 writes back to variable 1 and Thread 2 also writes back to variable 1, one more time. For a better understanding, please have a look at the below image.

Atomic Methods, Thread Safety, and Race Conditions in C# with Examples

This means that, as you can see, depending on the order of the execution of the methods, we are going to determine the value of the variable. So, even though we increase the value twice in different threads because we were in a multithreaded environment, then we had a Race condition, which means that now we don’t have a deterministic operation because sometimes it could be one. Sometimes the value of the variable could be two. It all depends on chance.

How to Solve the above Problem in C#?

We can use synchronization mechanisms. There are many ways to resolve the above problem. The first mechanism that we’re going to look at to deal with the problems of having a variable edited by multiple threads is Interlocked. Then we will see how to use lock to solve the race condition problem.

Interlocked in C#:

The Interlocked Class in C# allows us to perform certain operations in an atomic way, which makes this operation safe to do from different threads on the same variable. That means Interlocked class gives us a few methods that allow us to perform certain operations safely or atomically, even if the code is going to be executed by several threads simultaneously.

Example to Understand Interlocked in C#:

First, we will see the example without using Interlocked and see the problem, and then we will rewrite the same example using Interlocked and will see how interlocked solve the thread safety problem.

Please have a look at the following example. In the below example, we have declared a variable and by using the Parallel For loop we are incrementing the value. As we know Parallel.For loop uses multithreading so multiple threads trying to update (increment) the same ValueWithoutInterlocked variable. Here, as we are looping for 100000 times so we are expecting the value of the ValueWithoutInterlocked to be 100000.

using System;
using System.Threading.Tasks;

namespace ParallelProgrammingDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            var ValueWithoutInterlocked = 0;
            Parallel.For(0, 100000, _ =>
            {
                //Incrementing the value
                ValueWithoutInterlocked++;
            });
            Console.WriteLine("Expected Result: 100000");
            Console.WriteLine($"Actual Result: {ValueWithoutInterlocked}");
            Console.ReadKey();
        }
    }
}

Now, run the above code multiple times and you will get different results each time, and you can also see the difference between the Actual Result and the Expected Result as shown in the below image.

Interlocked in C#

Example using Interlocked Class in C#:

The Interlocked Class in C# provides one static method called Increment. The Increment method increments a specified variable and stores the result, as an atomic operation. So, here we need to specify the variable with the ref keyword as shown in the below example.

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

namespace ParallelProgrammingDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            var ValueInterlocked = 0;
            Parallel.For(0, 100000, _ =>
            {
                //Incrementing the value
               Interlocked.Increment(ref ValueInterlocked);
            });
            Console.WriteLine("Expected Result: 100000");
            Console.WriteLine($"Actual Result: {ValueInterlocked}");
            Console.ReadKey();
        }
    }
}
Output:

Example using Interlocked Class in C#

As you can see in the above output image, we are getting the Actual Result as the Expected Result. So, the Interlocked Class provides atomic operations for variables that are shared by multiple threads. That means the synchronization mechanism Interlocked allows us to avoid having race conditions by making the increment operation Atomic. If you go to the definition of Interlocked class, you will see that this class provides many static methods such as Increment, Decrement, Add, Exchange, etc as shown in the below image to perform atomic operations on the variable.

Interlocked Class in C#

Sometimes Interlocked is not enough. Sometimes we don’t multiple threads to access the critical section. We want only one thread to access the critical section. For that, we can use the lock.

Lock in C#:

Another mechanism that we can use for data editing by multiple threads simultaneously is a lock. with lock, we can have a block of code that will only be executed by one thread at a time. That is, we limit a part of our code to be sequential, even if several threads try to execute that code at the same time. We use locks when we need to perform several operations or an operation not covered by Interlocked.

Something important to take into account is that ideally what we do inside a lock block should be relatively fast. This is because the threads are blocked while waiting for the release of the lock. And if you have multiple threads blocked for a longer period of time, this can have an impact on the speed of your application.

Example to Understand the Lock in C#:

Let us rewrite the previous example using the lock. Please have a look at the below example. It is recommended to have a dedicated object for the lock. The idea is that we make locks based on objects.

using System;
using System.Threading.Tasks;

namespace ParallelProgrammingDemo
{
    class Program
    {
        static object lockObject = new object();

        static void Main(string[] args)
        {
            var ValueWithLock = 0;
            Parallel.For(0, 100000, _ =>
            {
                lock(lockObject)
                {
                    //Incrementing the value
                    ValueWithLock++;
                }
            });
            Console.WriteLine("Expected Result: 100000");
            Console.WriteLine($"Actual Result: {ValueWithLock}");
            Console.ReadKey();
        }
    }
}
Output:

blank

In the next article, I am going to discuss Interlock vs Lock in C# with Examples. Here, in this article, I try to Atomic Methods, Thread Safety, and Race Conditions in C# with Examples. I hope you enjoy this Atomic Method, Thread Safety, and Race Conditions in C# with Examples.

1 thought on “Atomic Methods Thread Safety and Race Conditions in C#”

  1. blank

    Guys,
    Please give your valuable feedback. And also, give your suggestions about Atomic Methods, Thread Safety, and Race Conditions in the C# concept. If you have any better examples, you can also put them in the comment section. If you have any key points related to Atomic Methods, Thread Safety, and Race Conditions in C#, you can also share the same.

Leave a Reply

Your email address will not be published.