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:
- Read the current value of
orderNumber
. - Increment the value.
- 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