Spinlocks
A spinlock is a lock that causes a thread to wait in a loop while repeatedly checking if the lock is available. The thread “spins” in a tight loop until the lock becomes free, hence the name “spinlock”. Spinlocks are useful in situations where the lock is expected to be held for a short duration and the overhead of context switching is higher than the cost of spinning.
Here’s an example of implementing a spinlock in Rust using atomic primitives:
use std::sync::atomic::{AtomicBool, Ordering};
pub struct Spinlock {
locked: AtomicBool,
}
impl Spinlock {
pub fn new() -> Self {
Spinlock {
locked: AtomicBool::new(false),
}
}
pub fn lock(&self) {
while self.locked.swap(true, Ordering::Acquire) {
std::hint::spin_loop();
}
}
pub fn unlock(&self) {
self.locked.store(false, Ordering::Release);
}
}
In this example, the Spinlock
struct contains an AtomicBool
called locked
. The lock
method uses the swap
operation to atomically set the locked
flag to true
and returns the previous value. If the previous value was true
, indicating that the lock was already held, the thread enters a tight loop and spins until the lock becomes available. The unlock
method simply sets the locked
flag to false
, releasing the lock.
Atomic Flags
Atomic flags are boolean variables that support atomic operations, such as setting, clearing, and testing the flag value. They provide a means to synchronize access to a shared boolean state across multiple threads.
Here’s an example of using atomic flags in Java:
import java.util.concurrent.atomic.AtomicBoolean;
public class AtomicFlagExample {
private static final AtomicBoolean flag = new AtomicBoolean(false);
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
while (!flag.get()) {
// Wait for the flag to be set
}
System.out.println("Thread 1: Flag is set");
});
Thread thread2 = new Thread(() -> {
System.out.println("Thread 2: Setting the flag");
flag.set(true);
});
thread1.start();
thread2.start();
}
}
In this example, we have two threads: thread1
and thread2
. thread1
waits for the flag
to be set by continuously checking its value using the get
method. thread2
sets the flag
to true
using the set
method. The atomic nature of the AtomicBoolean
class ensures that the flag is accessed and modified atomically, preventing race conditions.
Spinlocks vs. Mutexes
Spinlocks and mutexes are both synchronization primitives used to protect shared resources, but they have different characteristics and use cases.
Spinlocks are lightweight and have low overhead, making them suitable for short critical sections where the lock is expected to be released quickly. However, spinlocks can lead to wasted CPU cycles if the lock is held for a long time, as the threads continuously spin while waiting for the lock to become available.
Mutexes, on the other hand, are more heavyweight and have higher overhead compared to spinlocks. When a thread attempts to acquire a mutex that is already held by another thread, it is typically put to sleep by the operating system until the mutex becomes available. Mutexes are suitable for longer critical sections or when the lock may be held for an extended period.
Here’s an example of using a mutex in Go:
import (
"fmt"
"sync"
)
func main() {
var mutex sync.Mutex
counter := 0
var wg sync.WaitGroup
wg.Add(5)
for i := 0; i < 5; i++ {
go func() {
defer wg.Done()
mutex.Lock()
counter++
mutex.Unlock()
}()
}
wg.Wait()
fmt.Println("Counter:", counter)
}
In this example, we use a sync.Mutex
to protect the counter
variable from concurrent access. Each goroutine acquires the mutex lock using mutex.Lock()
, increments the counter, and then releases the lock using mutex.Unlock()
. The sync.WaitGroup
is used to wait for all goroutines to finish before printing the final value of the counter.