Does Interlocked Solve a Real-Life Problem?
You count on your app, but is your app counting right?

We’ve talked about Interlocked.Increment before, but let’s be honest, “thread-safe integer increments” sound like quite a specific concern, don’t they? Is this just something for the .NET Core team at Microsoft to worry about, or could it actually affect your business application? Let’s find out!

An Ordinary Day at the Burger Bar

Imagine we have kiosks for ordering burgers, all connected to the same server in the kitchen. We want each order to have a unique integer number. To implement this, we will reuse BurgerOrderService from the previous post, making just one slight modification: removing the static modifier from the orderNumber, since we no longer need it to be global for our experiment.

class BurgerOrderService
{
    private int orderNumber = 0;

    public int GetOrderNumber()
    {
        return ++orderNumber;
    }

    public int GetOrderNumberButThreadSafe()
    {
        return Interlocked.Increment(ref orderNumber);
    }
}

The experiment idea is simple: we generate 1,000 order numbers asynchronously, and all of them should be unique. We can’t use HashSet<int> to verify uniqueness because it’s not thread-safe, which could affect our results. A thread-safe option would be ConcurrentDictionary<int, byte>, where the order number is the key, and the value can be anything. But we’ll use ConcurrentBag<int> instead. While it doesn’t check for duplicates, we’ll turn this into an advantage by verifying that exactly 1,000 executions occurred.

Console.WriteLine("Total order numbers: {0}", bag.Count);
Console.WriteLine("Unique order numbers: {0}", bag.Distinct().Count());

The experiment

First, we want to verify our setup and make 1,000 order numbers synchronously:

var orderService = new BurgerOrderService();
var bag = new ConcurrentBag<int>();

for (var i = 0; i < 1000; i++)
{
    bag.Add(orderService.GetOrderNumber());
}

The result is great, 1,000 executions and 1,000 unique order numbers:

1,000 executions and 1,000 unique order numbers

Now, it’s time to answer our main question: can we see a race condition with the integer increment operator using a simple code snippet? Let’s find out. We want to execute BurgerOrderService methods asynchronously, so we need an auxiliary function, GetOrderNumberAsync. And using of Task.Delay is a little trick to force the method to be asynchronous and prevent the compiler from optimizing or inlining it:

async Task GetOrderNumberAsync(BurgerOrderService service, ConcurrentBag<int> bag)
{
    await Task.Delay(1);    // This forces the method to be async
    bag.Add(service.GetOrderNumber());
}

var tasks = new List<Task>();
for (var i = 0; i < 1000; i++)
{
    tasks.Add(GetOrderNumberAsync(orderService, bag));
}
await Task.WhenAll(tasks);

And the output is…

1,000 executions and 943 unique order numbers

Wow. Quite far away from 1,000. The number of unique orders will vary with each run, because race conditions can be different each time. It may even reach 1,000 on some executions, but the exact number can never be guaranteed.

As we can see, even with a simple code snippet and without diving deep into the system assemblies, we can encounter a race condition on the integer increment operator.

Can Interlocked fix it?

Now, the Interlocked.Increment enters the stage:

async Task GetOrderNumberThreadSafeAsync(BurgerOrderService service, ConcurrentBag<int> bag)
{
    await Task.Delay(1);    // This forces the method to be async
    bag.Add(service.GetOrderNumberButThreadSafe());
}

for (var i = 0; i < 1000; i++)
{
    tasks.Add(GetOrderNumberThreadSafeAsync(orderService, bag));
}

And we get a solid, thread-safe result:

1,000 executions and 1,000 unique order numbers

Exactly as expected.

The complete source code for this experiment is available here.

***

Our experiment shows that even a simple integer increment can lead to race conditions in a multi-threaded environment. So while we are using the power of asynchronous executions in .NET, we have to be careful even to these small details.


Last modified on 2025-02-09

Get new posts by email: