Back to Blog
C#

C# Interlocked: Atomic Operations Without Locks

c# interlocked: Learn how C# Interlocked provides atomic operations for thread-safe counters and lock-free updates without explicit locking.

Thread SafetyAtomic OperationsConcurrency.NETMultithreadingLock-Free Programming
Diagram showing multiple threads performing an atomic increment on a single shared counter using C# Interlocked

When multiple threads update the same variable, a simple expression like counter++ compiles to a read-modify-write sequence that is not atomic. Two threads can read the same value, increment it, and write back, losing one update. C# Interlocked provides atomic operations that perform these updates as a single unit, without requiring an explicit lock.

What Interlocked Solves in Multithreaded Code

The problem with counter = counter + 1 is that the runtime must read the current value, add one, and store the result. Between the read and the write, another thread can modify the same memory location. The result is a lost update, and the final value ends up lower than expected.

Interlocked operations are performed atomically at the hardware level. The processor guarantees that the read-modify-write sequence completes without interruption. This makes Interlocked the simplest way to protect simple numeric state shared between threads.

The Core Interlocked Operations

The Interlocked class exposes a small set of static methods that cover the operations most often needed in concurrent code:

MethodBehavior
IncrementAtomically adds 1 to an integer or long
DecrementAtomically subtracts 1 from an integer or long
AddAtomically adds a specified value to an integer or long
ExchangeAtomically replaces a value and returns the previous value
CompareExchangeAtomically replaces a value only if it matches a comparison value, and returns the original value
ReadAtomically reads a 64-bit value on 32-bit platforms

All of these methods accept ref parameters because they must operate on the original storage location, not a copy of the value.

int counter = 0; // Thread-safe increment Interlocked.Increment(ref counter);

The ref keyword is essential here. Passing a value by reference ensures the method operates on the same memory location that all threads share.

A Practical Example: Thread-Safe Counter

A common use case is a counter that tracks the number of processed items across multiple worker threads. Without Interlocked, the counter would lose updates under contention.

public class JobTracker { private int _completedJobs; public void MarkJobCompleted() { Interlocked.Increment(ref _completedJobs); } public int CompletedJobs => Volatile.Read(ref _completedJobs); }

The Interlocked.Increment call guarantees that each completed job is counted exactly once, regardless of how many threads call MarkJobCompleted concurrently. Reading the counter with Volatile.Read ensures the reader observes a current value rather than a stale cached one.

Using CompareExchange for Lock-Free Updates

CompareExchange is the most flexible Interlocked operation because it enables a pattern called compare-and-swap. You can update a value only if it still holds an expected value, which is the foundation of lock-free algorithms.

int current = _state; // Attempt to transition from current to next int observed = Interlocked.CompareExchange(ref _state, nextValue, current); if (observed == current) { // The update succeeded } else { // Another thread changed the value; retry or handle the conflict }

CompareExchange compares the current value of _state with current. If they match, it stores nextValue and returns the original value. If they do not match, it leaves the value unchanged and returns what the value actually was. The return value tells you whether the update succeeded, which lets you retry in a loop when the state is changing rapidly.

This pattern is used to implement lock-free stacks, queues, and lazy initialization without blocking threads.

Memory Ordering Guarantees

Interlocked operations in .NET provide full memory barriers. A memory barrier prevents the processor and the compiler from reordering memory operations across the barrier. This means that writes performed before an Interlocked operation are visible to other threads after that operation completes, and reads performed after an Interlocked operation see values written before it.

This guarantee is stronger than what a simple volatile read or write provides. It is the reason Interlocked can be used to coordinate access to shared state beyond the single variable being modified. If you need to publish a reference and a set of fields together, an Interlocked exchange of the reference makes the fields visible to other threads once they observe the new reference.

Interlocked vs. lock: Choosing the Right Tool

A lock statement protects a larger critical section and can guard multiple variables or complex logic. Interlocked protects only a single memory location with a single operation. The tradeoff is between flexibility and overhead.

Use lock when the update involves multiple variables, requires a read-modify-write that Interlocked does not provide directly, or needs to coordinate several operations as one logical unit.

Use Interlocked when the operation is a simple increment, decrement, add, exchange, or compare-and-swap on a single value. Interlocked avoids the overhead of acquiring and releasing a monitor, which reduces contention and improves throughput in high-concurrency scenarios.

There is no benchmark number that applies universally here. The choice depends on how much work the critical section performs. If the critical section is a single arithmetic operation, Interlocked is almost always the better choice. If the section contains multiple statements, a lock is simpler to reason about and less error-prone than trying to compose several Interlocked calls.

Limitations and Common Mistakes

Interlocked does not make arbitrary expressions atomic. Interlocked.Increment(ref value) is atomic, but value = value + 1 is not, even if it appears in a single line of C#. You cannot wrap a compound expression such as value = value * 2 + offset in an Interlocked call; you would need a compare-and-swap loop or a lock.

Another common mistake is using Interlocked on a field that is not shared, or forgetting the ref keyword, which causes a compile error because the method requires a variable, not a value. Also, Interlocked works with value types such as int, long, and references, but not with decimal, double arithmetic beyond exchange, or custom structs.

On 32-bit platforms, reading a 64-bit long is not atomic without Interlocked.Read. On 64-bit platforms, reads of aligned 64-bit values are atomic, but using Interlocked.Read keeps the code correct across both architectures.

The final consideration is that Interlocked operations are still subject to the same memory model rules as other .NET code. They provide atomicity and ordering for the operation itself, but they do not replace the need to design the surrounding algorithm correctly. Lock-free code built on CompareExchange requires careful reasoning about retry loops and the conditions under which a thread may observe stale values.

c# interlocked: Practical Usage and Code Examples | RYUSLOG DEV