In this lesson, you will learn how to design custom synchronization primitives by leveraging atomic operations to control hardware-level thread contention.
When standard library locks like std::sync::Mutex are too heavy due to OS-level context switching, you can implement a Spinlock. A spinlock is a low-level primitive that keeps a thread in a tight loop ("spinning") until a resource becomes available, avoiding the overhead of putting the thread to sleep.
Implementing a Basic Spinlock
To implement this, we use an AtomicBool combined with the compare_exchange operation, which allows us to check and update the lock state atomically.
use std::sync::atomic::{AtomicBool, Ordering};
use std::cell::UnsafeCell;
pub struct Spinlock<T> {
locked: AtomicBool,
data: UnsafeCell<T>,
}
unsafe impl<T: Send> Sync for Spinlock<T> {}
impl<T> Spinlock<T> {
pub fn lock(&self) {
// Attempt to move from false to true
while self.locked.compare_exchange_weak(
false, true, Ordering::Acquire, Ordering::Relaxed
).is_err() {
// Spin: thread remains active, keeping its CPU core
}
}
pub fn unlock(&self) {
self.locked.store(false, Ordering::Release);
}
}
The Role of Memory Ordering
The magic here lies in Ordering. By using Ordering::Acquire on the lock operation, we ensure that subsequent memory operations cannot be reordered before the lock is acquired. Conversely, Ordering::Release ensures all memory operations within the critical section are finished before the lock is made visible to other threads.
When to Use Custom Locks
Custom primitives excel in "hot" workloads where the critical section is extremely short—often faster than the time it takes to notify the kernel to deschedule the thread. However, they are dangerous: if a thread holding a spinlock is preempted by the OS, or if the lock is held too long, you will waste significant CPU cycles, leading to high power consumption and performance degradation. Always ensure your critical sections are minimal and avoid blocking operations (like I/O) within them.
Key Takeaway: Custom synchronization primitives replace kernel-level context switching with busy-waiting loops, providing massive performance gains for ultra-short critical sections at the cost of higher CPU utilization.