One of the most common mistakes in multithreaded programming is assuming that operations on in-memory variables are automatically atomic.
They are not.
Even a simple statement like counter++; is actually composed of multiple steps:
- Read the current value
- Increment the value
- Write the new value back
If multiple threads execute this simultaneously, race conditions can occur and updates may be lost.
For atomic integer operations
1- Interlocke (Lightweight atomic operations on numeric operations)
Interlocked provides lightweight atomic operations without requiring explicit locks, making it ideal for counters, statistics, and shared numeric state.
private int _value;
Interlocked.Increment(ref _value);
Interlocked.Decrement(ref _value);
Interlocked.Add(ref _value, 5);
//Reading safely:
int current = Volatile.Read(ref _value);
This is the standard solution for thread-safe integer mutation.
2- ReaderWriterLockSlim. For many readers / single writer semantics
useful when:
- many threads read data frequently
- writes are relatively rare
- multiple operations must be protected together
private readonly ReaderWriterLockSlim _lock = new(); private int _value; public int Read() { _lock.EnterReadLock(); try { return _value; } finally { _lock.ExitReadLock(); } } public void Increment() { _lock.EnterWriteLock(); try { _value++; } finally { _lock.ExitWriteLock(); } }
This gives:
- multiple concurrent readers
- only one writer at a time
which is useful for read-heavy workloads.
Creating a Reusable Concurrent Integer
You can also encapsulate atomic operations in a reusable type:
public class ConcurrentInt
{
private int _value;
public ConcurrentInt(int initialValue = 0)
{
_value = initialValue;
}
public int Value => Volatile.Read(ref _value);
public int Increment() => Interlocked.Increment(ref _value);
public int Decrement() => Interlocked.Decrement(ref _value);
public int Add(int amount) => Interlocked.Add(ref _value, amount);
}
Usage:
var x = new ConcurrentInt(14);
x.Increment();
Console.WriteLine(x.Value);
Summery
- ++ and -- are not atomic
- race conditions can occur under concurrency
- Interlocked is the preferred solution for atomic primitive updates
- ReaderWriterLockSlim is better for protecting larger shared state
- never assume in-memory operations are thread-safe by default