C# SpinLock and SpinWait for Low-Latency Spinning
In high-performance and low-latency C# applications, traditional
thread synchronization primitives like Monitor (the
lock keyword), Mutex, or
AutoResetEvent introduce costly kernel transitions and
thread context switches. The System.Threading.SpinLock and
System.Threading.SpinWait value types solve this problem by
keeping waiting threads active on the CPU in user mode—a process called
"busy-waiting" or "spinning." This article explores the purpose,
internal mechanics, and appropriate use cases of both structures when
optimizing for ultra-short wait times.
The Cost of Standard Thread Synchronization
Standard synchronization mechanisms put a waiting thread into a blocked state when a resource is contended. When the resource becomes available, the operating system wakes the blocked thread and switches its execution context back onto a core.
While blocking frees the CPU to perform other work, the context switch itself typically costs thousands of CPU cycles (microseconds). If a shared critical section only requires a few dozen CPU cycles to complete, the overhead of putting a thread to sleep and waking it up is disproportionately expensive. In low-latency scenarios, it is significantly faster to keep the CPU core actively executing a loop until the lock frees up.
Understanding SpinWait
SpinWait is a lightweight struct designed to orchestrate
low-level spinning without completely starving the CPU. Instead of
executing an unyielding infinite loop, it implements a stepped back-off
strategy.
Key characteristics of SpinWait include:
- Two-Phase Back-Off: During initial iterations,
SpinWaitexecutes brief CPU-level pauses using instructions likePAUSEon x86/x64 processors viaThread.SpinWait. This prevents CPU pipeline stalls and reduces power consumption. - Progressive Yielding: If the wait duration extends
beyond a threshold,
SpinWaitautomatically escalates to yielding execution to other threads viaThread.Yield()andThread.Sleep(0). On single-core machines, it skips pure spinning entirely to avoid deadlocking the thread that holds the expected state. - Implementation Pattern: It is generally used in
lock-free algorithms (such as CAS operations via
Interlocked) to retry state modifications.
var spinWait = new SpinWait();
while (!IsResourceAvailable())
{
spinWait.SpinOnce();
}Understanding SpinLock
SpinLock is a low-level, exclusive mutual-exclusion
synchronization primitive built specifically for ultra-short critical
sections. Like SpinWait, it avoids operating system context
switching by spinning until the lock is acquired.
Key characteristics of SpinLock include:
- Struct Semantics:
SpinLockis a value type. To avoid accidental copying (which results in separate, broken locks), it must be passed by reference using therefkeyword. - Thread Tracking: It optionally tracks the owning
thread ID. While this aids in debugging and detects recursive locks
(which causes self-deadlocks with spinning locks), tracking introduces
slight overhead and can be disabled via the constructor
(
new SpinLock(enableThreadOwnerTracking: false)). - Deterministic Acquisition: Acquisition requires passing a boolean flag by reference, ensuring robust exception handling even if an exception occurs mid-entry.
private SpinLock _spinLock = new SpinLock(false);
public void ExecuteCriticalWork()
{
bool lockTaken = false;
try
{
_spinLock.Enter(ref lockTaken);
// Critical section: Must execute in nanoseconds
}
finally
{
if (lockTaken)
{
_spinLock.Exit();
}
}
}When to Use
SpinLock and SpinWait
Both structures are specialized tools that must be used strictly under the right conditions:
- Critical Section Duration: The protected section or wait condition must complete almost instantaneously (ideally under a few microseconds or tens of CPU cycles).
- Contention Levels: Contention must be low to moderate. High contention leads to multiple threads pinning CPU cores at 100% capacity without doing useful work.
- Multi-Core Environments: Pure spinning is only effective on multi-core architectures where the holding thread and the spinning thread can execute simultaneously on separate cores.
- No Blocking Operations: Code inside a
SpinLockor within aSpinWaitloop must never perform I/O, allocate large memory blocks, or call blocking methods.