Interlocked and thread-safe counters
Are your integers and longs thread-safe?

Static variables are a convenient way to share data between threads in C#. While thread safety is often considered when working with complex objects, it’s easy to overlook when dealing with seemingly simple types like integers or longs. Sure, C# has built-in thread-safe solutions like ConcurrentDictionary for complex scenarios, but integers and longs might feel so “atomic” that we forget about thread safety when using them. This oversight can lead to unexpected (and buggy) behavior. Thankfully, we’ve got the Interlocked class from System.Threading to solve these issues easily.

Imagine we have two kiosks for ordering burgers, both connected to the same server in the kitchen. We want each order to have a unique number. For some reason, the folks in the kitchen don’t like GUIDs for order numbers—they prefer something human-readable. It would also be nice to have order IDs as a consequence so the customers can expect when they will be served. Here’s one way to implement it:

class BurgerOrderService
{
    private static int orderNumber = 1;

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

But will it work when two orders are placed at the same time? Unfortunately, no. The ++ operator isn’t atomic; it actually breaks down into three separate steps:

  1. Read the current value of orderNumber.
  2. Increment the value.
  3. Write the updated value back to orderNumber.

Without synchronization, two (or more) threads could execute these steps simultaneously, resulting in a race condition. For example:

  • Thread A reads orderNumber as 5.
  • Thread B also reads orderNumber as 5.
  • Thread A increments and writes 6.
  • Thread B increments and writes 6 (overwriting Thread A’s update).

Now we have two orders with the same number, and that’s no good—kitchen chaos, unhappy customers, the whole deal.

Making It Thread-Safe

We could fix this by using a lock or even semaphores:

class BurgerOrderService
{
    private static int orderNumber = 1;
    private static readonly object _lock = new object();

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

Here we use a lock statement with a private _lock object to ensure that only one thread at a time can execute the code that increments orderNumber. This guarantees thread safety but introduces a performance overhead due to the kernel-level locking mechanism.

Interlocked

In our case, we simply want to increment an integer with as little overhead as possible. And that’s where the Interlocked class comes to the rescue. According to the docs, Interlocked ‘provides atomic operations for variables that are shared by multiple threads.’ That’s exactly what we need! Here is the updated implementation:

class BurgerOrderService
{
    private static int orderNumber = 0;

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

With Interlocked.Increment, the increment and return happen as a single atomic operation. Problem solved—thread-safe and efficient!

What’s Happening Under the Hood?

If we look closer, there’s still locking involved, but now it’s happening at the CPU level. On x86/x64 architectures, the LOCK XADD command is used, and on ARM, it’s LDREX/STREX. This kind of locking avoids the overhead of kernel-level locks, making it much faster. That’s why Interlocked is widely used throughout the .NET System assemblies.

Wrapping Up

Interlocked.Increment is a quick and easy way to make your code thread-safe when incrementing shared integers or longs. It keeps things simple and avoids the overhead of locks by working directly at the CPU level. Faster and safer? Yes, please!

But Interlocked isn’t just about counting. It also has your back for other operations like adding, subtracting, or even doing bitwise magic. If you ever find yourself needing to update a value conditionally or swap shared variables without breaking a sweat, check out CompareExchange and Exchange.

So, next time you’re writing multithreaded code, remember: Interlocked is your friend. It’s there to help you avoid headaches and keep your code running smoothly.


Last modified on 2024-12-14