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 LOCK prefix before specific assembly instructions (such as LOCK XADD for atomic addition or LOCK CMPXCHG for compare-and-swap). The LOCK prefix 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 (LDREX and STREX), 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:

  1. Compares a target memory location with an expected value.
  2. If the values are identical, overwrites the target with a new value.
  3. 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 like LOCK 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:

  1. Instruction Ordering: It prevents the compiler and CPU from reordering memory reads and writes across the barrier boundary.
  2. 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.