How C# Interlocked Works Without Locks
The Interlocked class in C# provides thread-safe, atomic
operations on shared variables without relying on traditional software
locks or monitors. Instead of forcing threads to block and enter
expensive kernel transitions, Interlocked maps directly to
hardware-level CPU instructions that guarantee operations like
increments, decrements, and swaps are completed in a single,
uninterrupted step. This article explains how Interlocked
achieves lock-free synchronization, the role of processor-level
instructions like Compare-And-Swap (CAS), and how memory barriers ensure
visibility across CPU cores.
The Problem with Traditional Locks
Traditional synchronization mechanisms, such as the lock
statement (which uses Monitor) or primitive synchronization
objects like Mutex and Semaphore, involve
operating system intervention. When a thread encounters an acquired
lock, it may be placed in a wait state, triggering a context switch.
Context switches require the OS to save thread state, switch registers,
and reschedule execution, consuming thousands of CPU cycles.
For simple tasks like incrementing a counter or updating a reference, this overhead severely limits throughput and scalability in high-concurrency environments.
Hardware-Level Atomic Instructions
The Interlocked class avoids software-based locking
entirely by pushing synchronization directly to the CPU. Modern
processors (such as x86, x64, and ARM) include specialized hardware
instructions designed for atomic state modification:
- x86/x64 Architectures: The CPU uses the
LOCKprefix before specific assembly instructions (such asLOCK XADDfor atomic addition orLOCK CMPXCHGfor compare-and-swap). TheLOCKprefix asserts a signal on the system bus or, more commonly in modern multi-core chips, uses cache coherency protocols (like MESI) to lock down the specific cache line containing the target variable. This prevents other processor cores from reading or writing that memory location until the instruction finishes. - ARM Architectures: ARM chips typically achieve
atomicity via Load-Linked/Store-Conditional (LL/SC) instruction pairs
(
LDREXandSTREX), ensuring that a store succeeds only if no other processor has updated the memory location since the load.
Because the synchronization happens in hardware at the silicon level, execution takes only a few CPU cycles without pausing threads or engaging the OS kernel.
The Foundation: Compare-and-Swap (CAS)
The cornerstone of lock-free programming in the
Interlocked class is
Interlocked.CompareExchange. This method implements the
atomic Compare-and-Swap (CAS) primitive.
CompareExchange performs three actions atomically:
- Compares a target memory location with an expected value.
- If the values are identical, overwrites the target with a new value.
- Returns the original value stored in the target memory location.
If another thread modifies the variable during this window, the comparison fails, alerting the caller that the operation was not applied. Developers use this in a lock-free retry loop (optimistic concurrency):
int initialValue, computedValue;
do
{
initialValue = sharedVariable;
computedValue = initialValue * 2; // Arbitrary complex calculation
}
while (Interlocked.CompareExchange(ref sharedVariable, computedValue, initialValue) != initialValue);This pattern guarantees that the state is only updated if it remained unchanged throughout the calculation, eliminating race conditions without blocking.
Common Interlocked Operations
The Interlocked class exposes several heavily optimized
methods for common operations:
Increment/Decrement: Adds or subtracts 1 from a variable atomically using instructions likeLOCK XADD.Add: Atomically adds a specific integer to a variable and returns the new value.Exchange: Replaces the value of a variable with a new value atomically and returns the original value.Read: Atomically reads a 64-bit value on 32-bit systems, where 64-bit operations are normally non-atomic and susceptible to "torn reads."
Memory Barriers and Cache Coherence
Multi-core processors and optimizing compilers frequently reorder instructions and store values in core-local registers or CPU caches to maximize performance. Without coordination, an update made by Core A might not immediately be visible to Core B.
Methods in the Interlocked class generate a full memory
barrier (or memory fence). A memory barrier enforces two critical
rules:
- Instruction Ordering: It prevents the compiler and CPU from reordering memory reads and writes across the barrier boundary.
- Cache Coherency: It forces local CPU cache writes to flush out and invalidates stale cache entries on other cores, guaranteeing immediate read-write visibility across all threads.
Summary
The Interlocked class achieves high-performance thread
safety by discarding software-managed waiting queues in favor of
processor-level atomic instructions. By locking cache lines rather than
execution threads, it eliminates context-switch overhead, minimizes
contention, and guarantees memory consistency across modern multi-core
architectures.