Solving problems with race conditions and critical sections in C#

A race condition in C# occurs when two or more threads access shared data at the same time, and the program's output depends on the unpredictable execution order of those threads. This can lead to inconsistent or incorrect results, making race condition problems critical in multi-threaded applications.

In this article we will study everything in practice and try not only to understand, but also to solve the problems of race condition and critical sections in .NET.

How do race conditions occur? Race conditions usually occur when the following conditions are met:

  1. Shared resource: Two or more threads attempt to read, write, or modify the same shared resource (such as a variable, object, or file).

  2. Parallel execution: Threads execute in parallel without using appropriate synchronization mechanisms to control access to the shared resource. Race conditions occur when multiple threads simultaneously access and modify shared data, resulting in unpredictable results.

They arise from a lack of synchronization when threads are intertwined in such a way that operations are not performed in the expected order.

If you don't like reading articles, here is my YouTube video where I explain everything step by step.

A critical section in C# refers to a block of code that should only be executed by one thread at a time to prevent data corruption or inconsistent results due to concurrency. When multiple threads access shared resources, such as variables or objects, and at least one thread modifies those resources, the critical section ensures that only one thread can execute a block of code that accesses the shared resource at a time. This is important to maintain the integrity of shared data.

Let's jump straight into our example below and explore both sides of the issue.

public class Transaction
{
    public bool IsDone { get; set; }
    public void Transfer(decimal amount)
    {
        if (!IsDone)//critical section
        {
            Console.WriteLine($"Transaction operation started in thread number = {Environment.CurrentManagedThreadId}");
            TransferInternally(amount);
            Console.WriteLine($"Transaction operation ended in thread number = {Environment.CurrentManagedThreadId}");
            IsDone = true;
        }
    }
    private void TransferInternally(decimal amount)
    {
        Console.WriteLine($"TransferInternally in thread...{Environment.CurrentManagedThreadId}, the amount = {amount}");
        Console.WriteLine($"TransferInternally is done in thread...{Environment.CurrentManagedThreadId}, the amount = {amount}");
    }
}

In the provided code, the critical section and race condition issue are related to how multiple threads can interact with the Transaction class, specifically the property IsDone and method Transfer.

Critical section

A critical section is a piece of code that accesses shared resources (in this case, the IsDone property) and should not be executed by more than one thread at a time. The critical section in our code is as follows:

if (!IsDone)//critical section
        {
            Console.WriteLine($"Transaction operation started in thread number = {Environment.CurrentManagedThreadId}");
            TransferInternally(amount);
            Console.WriteLine($"Transaction operation ended in thread number = {Environment.CurrentManagedThreadId}");
            IsDone = true;
        }

This block of code is critical because it checks and updates the property IsDone. If multiple threads access this code simultaneously without proper synchronization, inconsistent or incorrect behavior may result.

Race condition problem

Race condition occurs when the result of program execution depends on the timing or order of execution of threads. In this code, race condition can happen if multiple threads call the method at the same time Transfer.

Scenario leading to race condition

  1. Thread 1 checks the IsDone property and finds that it is false.

  2. Thread 1 continues the transaction, prints a start message, and enters the TransferInternally method.

  3. Thread 2 then checks the IsDone property before Thread 1 completes execution and also sees that it is false.

  4. Thread 2 also continues the transaction even though Thread 1 is already processing it.

Since both threads find the property IsDone equal to false before one of them has time to set its value to trueboth threads will commit the transaction, which is not the expected behavior. Property IsDone will be set to true only after both threads have completed their operations, resulting in an incorrect situation where the transaction will be executed twice.

This is what our main method looks like:

static void Main(string[] args)
{
    Transaction2 transaction = new Transaction2();
    for (int i = 0; i < 10; i++)
    {
        Task.Run(() =>
        {
            transaction.Transfer(3000);
        });
    }
    Console.ReadLine();
}

The provided code demonstrates how a race condition problem can occur when multiple threads try to execute a method at the same time Transfer class Transaction.

Code Explanation Transaction Instance\

Transaction transaction = new Transaction();

Here a single instance of the Transaction class is created. This instance is used by all threads that will be created in the subsequent loop.

Creating and executing tasks:

for (int i = 0; i < 10; i++)
{
    Task.Run(() =>
    {
        transaction.Transfer(3000);
    });
}

This loop is executed 10 times and each iteration starts a new task using Task.Run. Each task calls a method Transfer object transactiontrying to transfer the amount of 3000.

Task Everyone Task.Run Spawns a new thread (or uses one from the thread pool) to execute the code in the lambda expression. Thus, up to 10 threads can simultaneously execute the Transfer method.

Since there is no synchronization mechanism such as the operator lockmultiple threads can simultaneously execute this block of code. This causes multiple threads to do the translation, print start and finish messages, and set IsDone V true.

Race condition occurs because the check IsDone and subsequent operations are not atomic. This means that even if one thread installs IsDone V trueother threads may have already passed the check and are also performing the translation, causing what should be a one-time transaction to be executed multiple times.

As a result of the race condition, you may see output that indicates that the transaction was processed multiple times, even though the logic implies that this should only happen once. Each thread will print its messages to the console, indicating that multiple threads have entered the critical section and completed the transaction

The result of race condition in C#

The result of race condition in C#

To prevent race condition problems and grab the critical section, we will use the keyword lock. Of course, there are many ways to avoid these problems, but the simplest one is to use the keyword lock.

public class Transaction2
{
    public bool IsDone { get; set; }
    private static readonly object _object = new object();
    public void Transfer(decimal amount)
    {
        lock(_object)
        {
            if (!IsDone)//should act as a single atomic operation
            {
                Console.WriteLine($"Transaction operation started in thread number = {Environment.CurrentManagedThreadId}");
                TransferInternally(amount);
                Console.WriteLine($"Transaction operation ended in thread number = {Environment.CurrentManagedThreadId}");
                IsDone = true;
            }

        }

    }

    private void TransferInternally(decimal amount)
    {
        Console.WriteLine($"TransferInternally in thread...{Environment.CurrentManagedThreadId}, the amount = {amount}");
        Console.WriteLine($"TransferInternally is done in thread...{Environment.CurrentManagedThreadId}, the amount = {amount}");
    }
}
resolving race condition

resolving race condition

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *