Understanding Locks
Lock-Free Programming
Uses atomic operations instead of locks to coordinate between threads.
Techniques: - Compare-And-Swap (CAS): Atomic operation that updates a value only if it matches expected value - Load-Link/Store-Conditional (LL/SC): Similar to CAS but for larger data structures - Atomic operations: Increment, decrement, exchange, etc.
Example:
use std::sync::Arc;
use std::sync::atomic::{AtomicI64, Ordering};
use std::thread;
struct LockFreeCounter {
value: AtomicI64,
}
impl LockFreeCounter {
fn new() -> Self {
Self {
value: AtomicI64::new(0),
}
}
// Simple atomic increment
fn increment(&self) -> i64 {
self.value.fetch_add(1, Ordering::SeqCst)
}
// More complex: conditional update using compare-and-swap
fn increment_if_below(&self, max: i64) -> Result<i64, i64> {
loop {
let current = self.value.load(Ordering::SeqCst);
if current >= max {
return Err(current); // Already at max
}
let new_value = current + 1;
// Atomic compare-and-swap: only update if value is still 'current'
match self.value.compare_exchange(
current,
new_value,
Ordering::SeqCst,
Ordering::SeqCst,
) {
Ok(_) => return Ok(new_value), // Success
Err(_) => continue, // Someone else changed it, retry
}
}
}
fn get(&self) -> i64 {
self.value.load(Ordering::SeqCst)
}
}
fn main() {
let counter = Arc::new(LockFreeCounter::new());
let mut handles = vec![];
// Spawn 10 threads all incrementing the same counter
for i in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
for _ in 0..1000 {
counter.increment();
}
println!("Thread {} finished", i);
});
handles.push(handle);
}
// Wait for all threads
for handle in handles {
handle.join().unwrap();
}
println!("Final count: {}", counter.get()); // Should be 10,000
// Test conditional increment
let counter2 = LockFreeCounter::new();
match counter2.increment_if_below(5) {
Ok(v) => println!("Incremented to {}", v),
Err(v) => println!("Already at max, current: {}", v),
}
}Advantages: - No deadlocks possible - Better scalability in high-contention scenarios - Lower latency (no context switches) - Progress guaranteed (at least one thread makes progress)
Disadvantages: - Complex to implement correctly - Limited to simple data structures - ABA problem (value changes from A to B back to A) - Harder to reason about
Software Transactional Memory (STM)
Treats memory operations like database transactions with ACID properties.
How it works: - Mark beginning of transaction - Read/write to shared memory - Commit atomically or rollback on conflicts - System automatically handles conflicts
Example (Clojure/Haskell style):
-- Haskell STM example
transfer :: TVar Int -> TVar Int -> Int -> STM ()
transfer from to amount = do
fromBalance <- readTVar from
when (fromBalance < amount) $ error "Insufficient funds"
writeTVar from (fromBalance - amount)
toBalance <- readTVar to
writeTVar to (toBalance + amount)
-- Usage
main = atomically $ transfer account1 account2 100Advantages: - Composable transactions - No explicit locking - Automatic conflict resolution - Easier to reason about than manual locking
Disadvantages: - Performance overhead - Not widely supported in mainstream languages - Can have hidden retries - Limited to in-memory operations
Timestamp Ordering
Each transaction is assigned a timestamp, and operations are ordered by timestamp.
How it works: - Transaction gets timestamp at start - Read/write operations check timestamps - If operation violates timestamp order, transaction aborts
Example:
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
#[derive(Debug)]
struct AbortTransaction(String);
struct TimestampOrdering {
data: HashMap<String, String>,
read_ts: HashMap<String, u64>, // Latest read timestamp
write_ts: HashMap<String, u64>, // Latest write timestamp
counter: AtomicU64,
}
impl TimestampOrdering {
fn new() -> Self {
Self {
data: HashMap::new(),
read_ts: HashMap::new(),
write_ts: HashMap::new(),
counter: AtomicU64::new(0),
}
}
fn begin_transaction(&self) -> u64 {
self.counter.fetch_add(1, Ordering::SeqCst)
}
fn read(&mut self, ts: u64, key: &str) -> Result<Option<String>, AbortTransaction> {
let write_timestamp = self.write_ts.get(key).copied().unwrap_or(0);
if ts < write_timestamp {
return Err(AbortTransaction("Reading too old value".to_string()));
}
let current_read_ts = self.read_ts.get(key).copied().unwrap_or(0);
self.read_ts.insert(key.to_string(), current_read_ts.max(ts));
Ok(self.data.get(key).cloned())
}
fn write(
&mut self,
ts: u64,
key: &str,
value: String,
) -> Result<(), AbortTransaction> {
let read_timestamp = self.read_ts.get(key).copied().unwrap_or(0);
let write_timestamp = self.write_ts.get(key).copied().unwrap_or(0);
if ts < read_timestamp {
return Err(AbortTransaction("Too late to write".to_string()));
}
if ts < write_timestamp {
// Ignore write (Thomas Write Rule)
return Ok(());
}
self.data.insert(key.to_string(), value);
self.write_ts.insert(key.to_string(), ts);
Ok(())
}
}
// Usage example
fn main() {
let mut ts_system = TimestampOrdering::new();
// Transaction 1
let ts1 = ts_system.begin_transaction();
println!("Transaction 1 started with timestamp: {}", ts1);
// Transaction 1 writes
ts_system.write(ts1, "account_balance", "1000".to_string()).unwrap();
println!("Transaction 1 wrote: account_balance = 1000");
// Transaction 2 (later timestamp)
let ts2 = ts_system.begin_transaction();
println!("Transaction 2 started with timestamp: {}", ts2);
// Transaction 2 reads
match ts_system.read(ts2, "account_balance") {
Ok(Some(value)) => println!("Transaction 2 read: account_balance = {}", value),
Ok(None) => println!("Transaction 2: key not found"),
Err(e) => println!("Transaction 2 aborted: {:?}", e),
}
// Transaction 2 tries to write
match ts_system.write(ts2, "account_balance", "2000".to_string()) {
Ok(_) => println!("Transaction 2 wrote: account_balance = 2000"),
Err(e) => println!("Transaction 2 write aborted: {:?}", e),
}
// Transaction 0 (old timestamp) tries to write - should be rejected
match ts_system.write(0, "account_balance", "500".to_string()) {
Ok(_) => println!("Old transaction wrote (unexpected)"),
Err(e) => println!("Old transaction correctly aborted: {:?}", e),
}
}Advantages: - No deadlocks - No waiting (abort immediately if conflict) - Guarantees serializability
Disadvantages: - High abort rate under contention - Cascading aborts possible - Requires accurate clock synchronization in distributed systems
Mutex (Mutual Exclusion)
Mutex is a lock that is used to synchronize access to a shared resource. It is a binary lock, meaning that only one thread can acquire the lock at a time.
Characteristics: - Exclusive access - only one thread/process at a time - Binary state: locked or unlocked - Simple and straightforward
Example:
import threading
mutex = threading.Lock()
shared_counter = 0
def increment():
global shared_counter
mutex.acquire()
try:
shared_counter += 1
finally:
mutex.release()Read-Write Locks (Shared-Exclusive Locks)
Allows multiple readers but exclusive access for writers. This is a form of pessimistic locking that optimizes for read-heavy workloads.
Characteristics: - Multiple readers can hold the lock simultaneously - Writers require exclusive access - Better concurrency than pure mutex for read-heavy workloads
Example:
import threading
rw_lock = threading.RLock()
data = {}
def read_data(key):
with rw_lock: # Shared read
return data.get(key)
def write_data(key, value):
with rw_lock: # Exclusive write
data[key] = valueSemaphores
A generalization of mutex that allows N threads to access a resource simultaneously.
Characteristics: - Counter-based (can be > 1) - Useful for rate limiting or resource pools - Can control access to N identical resources
Example:
import threading
# Allow max 3 concurrent connections
connection_pool = threading.Semaphore(3)
def access_database():
with connection_pool:
# Only 3 threads can be here at once
execute_query()Spinlocks
A lock where the thread repeatedly checks if the lock is available (busy-waiting).
Characteristics: - No context switching (CPU intensive) - Very fast for short critical sections - Wastes CPU cycles if lock is held long
Use case: Kernel-level operations, very short critical sections