Overview
A deadlock occurs when two or more processes or threads wait indefinitely for resources held by each other, creating a circular dependency that prevents any of them from proceeding. In concurrent programming, deadlocks represent a critical failure mode where the system becomes stuck, requiring external intervention to resolve.
Deadlocks emerge from the fundamental challenge of resource management in concurrent systems. When multiple execution contexts compete for shared resources like memory, files, database connections, or locks, coordination mechanisms prevent race conditions but introduce the risk of circular waiting. A thread holding resource A while requesting resource B can deadlock with another thread holding B while requesting A.
# Simple deadlock scenario
mutex_a = Mutex.new
mutex_b = Mutex.new
thread1 = Thread.new do
mutex_a.synchronize do
sleep 0.1 # Simulate work
mutex_b.synchronize do
puts "Thread 1 completed"
end
end
end
thread2 = Thread.new do
mutex_b.synchronize do
sleep 0.1 # Simulate work
mutex_a.synchronize do
puts "Thread 2 completed"
end
end
end
thread1.join
thread2.join
# => Deadlock: both threads wait forever
The impact of deadlocks extends beyond frozen threads. Database systems experience transaction rollbacks, web servers stop responding to requests, and distributed systems lose coordination. Detection and recovery mechanisms add overhead, while prevention strategies constrain system design. Understanding deadlock conditions, prevention techniques, and Ruby-specific concurrency patterns enables building reliable concurrent applications.
Key Principles
Deadlock occurs when four conditions hold simultaneously, known as the Coffman conditions. Mutual exclusion means resources cannot be shared—only one thread accesses the resource at a time. Hold and wait means threads holding resources can request additional resources. No preemption means resources cannot be forcibly taken from threads—threads must release resources voluntarily. Circular wait means a circular chain of threads exists where each thread waits for a resource held by the next thread in the chain.
# Demonstrating the four conditions
class BankAccount
def initialize(balance)
@balance = balance
@mutex = Mutex.new
end
def transfer(amount, target_account)
@mutex.synchronize do # Acquires lock on source (hold)
sleep 0.01
target_account.deposit_locked(amount) # Waits for target lock (wait)
end
end
def deposit_locked(amount)
@mutex.synchronize do # Mutual exclusion enforced
@balance += amount
end
end
end
# Two accounts transferring to each other creates circular wait
account_a = BankAccount.new(1000)
account_b = BankAccount.new(1000)
t1 = Thread.new { account_a.transfer(100, account_b) }
t2 = Thread.new { account_b.transfer(100, account_a) }
# => Deadlock: circular wait between threads
Resource allocation graphs visualize deadlock situations. Nodes represent processes and resources, with directed edges showing requests and allocations. A cycle in the graph indicates potential deadlock when resources support only single allocation. If thread T1 holds resource R1 and requests R2, while thread T2 holds R2 and requests R1, the graph contains a cycle and deadlock exists.
Livelock differs from deadlock—threads remain active but make no progress. Threads continuously change state in response to each other without completing work. Two threads attempting to politely yield to each other can livelock, repeatedly deferring but never proceeding.
# Livelock example - both threads yield indefinitely
shared_flag = Atomic.new(0)
thread1 = Thread.new do
loop do
if shared_flag.value == 0
shared_flag.value = 1
puts "T1 working"
break
else
shared_flag.value = 0 # Yield to T2
sleep 0.001
end
end
end
thread2 = Thread.new do
loop do
if shared_flag.value == 1
shared_flag.value = 0
puts "T2 working"
break
else
shared_flag.value = 1 # Yield to T1
sleep 0.001
end
end
end
# => May livelock if timing aligns poorly
Starvation occurs when a thread perpetually fails to acquire necessary resources, blocked by other threads that repeatedly acquire them first. Unlike deadlock, the system makes progress but specific threads cannot proceed. Priority-based scheduling can cause starvation when high-priority threads continuously preempt lower-priority threads.
Ruby Implementation
Ruby provides thread synchronization primitives through the Mutex class, which implements mutual exclusion locks. The synchronize method acquires the lock, executes the block, and releases the lock even if exceptions occur. The lock and unlock methods provide manual control but require careful exception handling to prevent lock leakage.
require 'thread'
class ThreadSafeCounter
def initialize
@count = 0
@mutex = Mutex.new
end
def increment
@mutex.synchronize do
current = @count
sleep 0.001 # Simulate complex operation
@count = current + 1
end
end
def value
@mutex.synchronize { @count }
end
end
counter = ThreadSafeCounter.new
threads = 10.times.map do
Thread.new { 100.times { counter.increment } }
end
threads.each(&:join)
puts counter.value # => 1000 (correct with synchronization)
The Mutex class includes try_lock, which attempts to acquire the lock without blocking. This method returns true if the lock was acquired and false if another thread holds it. The non-blocking nature enables implementing timeout-based deadlock prevention.
class ResourcePool
def initialize
@resources = {}
@mutex = Mutex.new
end
def acquire_resources(resource_ids, timeout = 5)
start_time = Time.now
acquired = []
resource_ids.each do |id|
# Try to acquire each resource with timeout
loop do
if @mutex.try_lock
@resources[id] = Thread.current
acquired << id
@mutex.unlock
break
end
if Time.now - start_time > timeout
# Timeout: release acquired resources
release_resources(acquired)
return false
end
sleep 0.01
end
end
true
end
def release_resources(resource_ids)
@mutex.synchronize do
resource_ids.each { |id| @resources.delete(id) }
end
end
end
The Monitor class from the standard library provides condition variables for complex synchronization patterns. Monitors combine mutual exclusion with condition signaling, allowing threads to wait for specific conditions while releasing locks.
require 'monitor'
class BoundedQueue
def initialize(capacity)
@capacity = capacity
@queue = []
@monitor = Monitor.new
@not_full = @monitor.new_cond
@not_empty = @monitor.new_cond
end
def enqueue(item)
@monitor.synchronize do
# Wait if queue is full
while @queue.size >= @capacity
@not_full.wait
end
@queue.push(item)
@not_empty.signal # Wake up waiting consumers
end
end
def dequeue
@monitor.synchronize do
# Wait if queue is empty
while @queue.empty?
@not_empty.wait
end
item = @queue.shift
@not_full.signal # Wake up waiting producers
item
end
end
end
Ruby's MRI implementation uses a Global Interpreter Lock (GIL) that prevents true parallel execution of Ruby code. Only one thread executes Ruby code at a time, though threads can run concurrently during I/O operations. The GIL reduces some race condition risks but deadlocks still occur when threads compete for application-level mutexes.
# GIL behavior demonstration
start = Time.now
# CPU-bound threads don't benefit from parallelism in MRI
threads = 4.times.map do
Thread.new do
total = 0
1_000_000.times { total += 1 }
end
end
threads.each(&:join)
cpu_time = Time.now - start
puts "CPU-bound time: #{cpu_time}"
# I/O-bound threads can run concurrently
start = Time.now
threads = 4.times.map do
Thread.new do
sleep 0.5 # Simulates I/O operation
end
end
threads.each(&:join)
io_time = Time.now - start
puts "I/O-bound time: #{io_time}"
# => I/O time is ~0.5s, not 2s, showing concurrency during I/O
The Thread class provides raise to inject exceptions into threads, enabling deadlock recovery through forced interruption. The Thread.handle_interrupt method controls when threads process interrupts, preventing data corruption during critical sections.
Practical Examples
The dining philosophers problem illustrates classic deadlock scenarios. Five philosophers sit at a table with five forks. Each philosopher needs two forks to eat but only picks up one at a time. If all philosophers simultaneously pick up their left fork, none can pick up their right fork, creating deadlock.
class DiningPhilosophers
def initialize(philosopher_count)
@forks = philosopher_count.times.map { Mutex.new }
@philosophers = philosopher_count
end
def philosopher_routine(id)
left_fork = @forks[id]
right_fork = @forks[(id + 1) % @philosophers]
loop do
think
# Deadlock-prone approach: acquire left then right
left_fork.synchronize do
puts "Philosopher #{id} picked up left fork"
sleep 0.1 # Increases deadlock probability
right_fork.synchronize do
puts "Philosopher #{id} picked up right fork"
eat(id)
end
end
end
end
def think
sleep rand(0.1..0.3)
end
def eat(id)
puts "Philosopher #{id} is eating"
sleep rand(0.1..0.3)
end
end
# Run simulation - will deadlock
table = DiningPhilosophers.new(5)
threads = 5.times.map { |i| Thread.new { table.philosopher_routine(i) } }
threads.each(&:join)
Database transaction deadlocks occur when transactions acquire locks in different orders. Transaction T1 locks row A and requests row B while transaction T2 locks row B and requests row A. Databases typically detect these deadlocks and abort one transaction.
require 'pg'
class BankTransfer
def initialize(db_connection)
@conn = db_connection
end
def transfer_deadlock_prone(from_id, to_id, amount)
@conn.transaction do
# Lock source account
from_balance = @conn.exec_params(
"SELECT balance FROM accounts WHERE id = $1 FOR UPDATE",
[from_id]
)[0]['balance'].to_i
sleep 0.1 # Simulate processing time
# Lock destination account - potential deadlock point
to_balance = @conn.exec_params(
"SELECT balance FROM accounts WHERE id = $1 FOR UPDATE",
[to_id]
)[0]['balance'].to_i
@conn.exec_params(
"UPDATE accounts SET balance = $1 WHERE id = $2",
[from_balance - amount, from_id]
)
@conn.exec_params(
"UPDATE accounts SET balance = $1 WHERE id = $2",
[to_balance + amount, to_id]
)
end
rescue PG::DeadlockDetected => e
puts "Deadlock detected: #{e.message}"
retry
end
end
# Two concurrent transfers in opposite directions cause deadlock
conn1 = PG.connect(dbname: 'bank')
conn2 = PG.connect(dbname: 'bank')
t1 = Thread.new { BankTransfer.new(conn1).transfer_deadlock_prone(1, 2, 100) }
t2 = Thread.new { BankTransfer.new(conn2).transfer_deadlock_prone(2, 1, 50) }
# => One transaction gets rolled back by deadlock detector
File locking scenarios demonstrate system-level deadlocks. Process A locks file1.txt and requests file2.txt while process B locks file2.txt and requests file1.txt. Operating system file locks enforce mutual exclusion across processes, creating deadlock potential.
require 'fileutils'
class FileProcessor
def process_files_deadlock_prone(file1, file2)
File.open(file1, 'r+') do |f1|
f1.flock(File::LOCK_EX)
puts "Locked #{file1}"
sleep 0.1
File.open(file2, 'r+') do |f2|
f2.flock(File::LOCK_EX)
puts "Locked #{file2}"
# Process files
data1 = f1.read
data2 = f2.read
f1.write(data2)
f2.write(data1)
end
end
end
def process_files_safe(file1, file2)
# Lock in consistent order
first, second = [file1, file2].sort
File.open(first, 'r+') do |f1|
f1.flock(File::LOCK_EX)
File.open(second, 'r+') do |f2|
f2.flock(File::LOCK_EX)
# Safe to process
puts "Processing #{file1} and #{file2}"
end
end
end
end
processor = FileProcessor.new
t1 = Thread.new { processor.process_files_safe('a.txt', 'b.txt') }
t2 = Thread.new { processor.process_files_safe('b.txt', 'a.txt') }
t1.join
t2.join
# => No deadlock: both threads lock files in same order
Resource ordering prevents deadlock by establishing a total ordering of resources and requiring threads to acquire resources in ascending order. This breaks the circular wait condition since no thread can request a lower-ordered resource while holding a higher-ordered one.
class OrderedLockManager
def initialize
@locks = {}
@lock_order = {}
@next_id = 0
@manager_mutex = Mutex.new
end
def create_lock(name)
@manager_mutex.synchronize do
lock_id = @next_id
@next_id += 1
@locks[name] = { id: lock_id, mutex: Mutex.new }
lock_id
end
end
def acquire_multiple(lock_names)
# Sort by lock ID to enforce ordering
sorted_names = lock_names.sort_by { |name| @locks[name][:id] }
sorted_names.each do |name|
@locks[name][:mutex].lock
end
begin
yield
ensure
# Release in reverse order
sorted_names.reverse.each do |name|
@locks[name][:mutex].unlock
end
end
end
end
manager = OrderedLockManager.new
manager.create_lock('database')
manager.create_lock('cache')
manager.create_lock('logger')
# Both threads acquire in same order - no deadlock
t1 = Thread.new do
manager.acquire_multiple(['cache', 'database', 'logger']) do
puts "Thread 1 working"
end
end
t2 = Thread.new do
manager.acquire_multiple(['logger', 'database', 'cache']) do
puts "Thread 2 working"
end
end
t1.join
t2.join
# => Completes successfully: ordered acquisition prevents deadlock
Design Considerations
Deadlock prevention strategies eliminate one or more Coffman conditions. Preventing mutual exclusion requires making resources shareable, which applies to read-only data but not mutable state. Preventing hold-and-wait requires acquiring all resources atomically before execution, reducing concurrency since threads must wait for all resources simultaneously. Allowing preemption means forcibly taking resources from threads, but this requires rollback mechanisms and works only for stateless resources. Preventing circular wait through resource ordering works consistently but constrains acquisition patterns.
class ResourceManager
# Prevention: acquire all resources atomically
def with_all_or_none(resource_ids)
acquired = []
@global_lock.synchronize do
resource_ids.each do |id|
if @resources[id].locked?
# Can't get all - release acquired and fail
acquired.each { |r| r.unlock }
return false
end
@resources[id].lock
acquired << @resources[id]
end
end
begin
yield
true
ensure
acquired.each(&:unlock)
end
end
end
Deadlock detection monitors the system for circular wait conditions. Detection algorithms periodically examine resource allocation graphs for cycles. When deadlock is detected, the system must abort one or more threads to break the cycle. Selection of which thread to abort considers factors like work completed, priority, and resources held. Ruby applications typically implement timeout-based detection rather than graph analysis.
class DeadlockDetector
def initialize(timeout_seconds)
@timeout = timeout_seconds
@acquisitions = {}
@mutex = Mutex.new
end
def with_timeout_detection(resource_id)
start_time = Time.now
@mutex.synchronize do
@acquisitions[Thread.current] = {
resource: resource_id,
start_time: start_time
}
end
begin
Timeout.timeout(@timeout) do
yield
end
rescue Timeout::Error
check_for_deadlock
raise DeadlockDetected, "Timeout waiting for resource #{resource_id}"
ensure
@mutex.synchronize do
@acquisitions.delete(Thread.current)
end
end
end
def check_for_deadlock
@mutex.synchronize do
waiting_threads = @acquisitions.size
if waiting_threads >= 2
puts "Potential deadlock: #{waiting_threads} threads waiting"
@acquisitions.each do |thread, info|
puts " Thread #{thread.object_id}: waiting #{Time.now - info[:start_time]}s"
end
end
end
end
end
Lock timeout strategies set maximum wait times for lock acquisition. Threads that cannot acquire locks within the timeout interval abort and retry. This prevents indefinite blocking but introduces false positives when locks are held legitimately longer than the timeout. Timeout values must balance deadlock detection speed against spurious aborts.
class TimeoutBasedLocking
def acquire_with_timeout(resources, timeout)
deadline = Time.now + timeout
acquired = []
resources.each do |resource|
remaining = deadline - Time.now
if remaining <= 0
# Timeout expired - release and fail
acquired.each(&:unlock)
return false
end
# Try to acquire with remaining time
unless try_lock_timed(resource, remaining)
acquired.each(&:unlock)
return false
end
acquired << resource
end
begin
yield
true
ensure
acquired.each(&:unlock)
end
end
def try_lock_timed(resource, timeout)
start = Time.now
loop do
return true if resource.try_lock
return false if Time.now - start > timeout
sleep 0.01
end
end
end
Two-phase locking protocols divide resource acquisition into distinct phases. The growing phase acquires locks but never releases them. The shrinking phase releases locks but never acquires them. This protocol prevents deadlock in database systems by ensuring transactions acquire all locks before releasing any, though it may reduce concurrency.
Trade-offs between safety and performance affect design choices. Coarse-grained locking with fewer locks reduces deadlock complexity but limits parallelism. Fine-grained locking increases parallelism but introduces more deadlock scenarios. Lock-free algorithms using atomic operations avoid deadlocks entirely but require careful design and may not suit all use cases.
Common Pitfalls
Nested lock acquisition without consistent ordering causes deadlock. Functions that acquire locks must document their locking requirements. Calling a lock-acquiring function while holding locks creates hidden dependencies. The caller may hold lock A and call a function that acquires lock B, while another thread holds B and calls a function acquiring A.
class BadNesting
def initialize
@lock1 = Mutex.new
@lock2 = Mutex.new
end
def operation_a
@lock1.synchronize do
# Hidden deadlock risk: what if helper needs lock2?
helper_function
end
end
def operation_b
@lock2.synchronize do
helper_function
end
end
def helper_function
# If this needs lock2, operation_a deadlocks with operation_b
@lock2.synchronize do
puts "Helper working"
end
end
end
# Better: document and enforce lock ordering
class GoodNesting
LOCK_ORDER = [:lock1, :lock2]
def acquire_in_order(needed_locks)
ordered = needed_locks.sort_by { |name| LOCK_ORDER.index(name) }
locks = ordered.map { |name| instance_variable_get("@#{name}") }
locks.each(&:lock)
begin
yield
ensure
locks.reverse.each(&:unlock)
end
end
end
Exception handling in critical sections requires careful lock management. Exceptions thrown between lock acquisition and release leave locks held indefinitely. The synchronize method handles this automatically, but manual lock and unlock calls need exception handling.
class UnsafeLocking
def risky_operation
@mutex.lock
perform_work # May raise exception
@mutex.unlock # Never reached if exception occurs
end
end
class SafeLocking
def safe_operation
@mutex.lock
begin
perform_work
ensure
@mutex.unlock # Always executed
end
end
# Or use synchronize
def safest_operation
@mutex.synchronize do
perform_work # Exception safe
end
end
end
Callback functions executing under locks create hidden lock dependencies. Callbacks may call user-provided code that acquires locks, creating ordering violations. The lock holder cannot know what locks the callback might acquire.
class CallbackDeadlock
def process_with_callback(&callback)
@lock.synchronize do
data = prepare_data
callback.call(data) # Callback might acquire other locks
end
end
end
# Safer: release lock before callback
class SafeCallbacks
def process_with_callback(&callback)
data = @lock.synchronize { prepare_data }
callback.call(data) # No lock held during callback
end
end
Timeout values too short cause false positives, aborting legitimate long-running operations. Timeouts too long delay deadlock detection. Variable system load makes fixed timeouts unreliable. Adaptive timeouts based on historical acquisition times work better but add complexity.
class AdaptiveTimeout
def initialize
@lock_times = Hash.new { |h, k| h[k] = [] }
@mutex = Mutex.new
end
def acquire_with_adaptive_timeout(resource_id)
timeout = calculate_timeout(resource_id)
start = Time.now
success = Timeout.timeout(timeout) do
yield
end
duration = Time.now - start
record_acquisition(resource_id, duration)
success
rescue Timeout::Error
false
end
def calculate_timeout(resource_id)
times = @mutex.synchronize { @lock_times[resource_id].dup }
return 5.0 if times.empty? # Default timeout
# Use 95th percentile plus margin
sorted = times.sort
percentile_95 = sorted[(sorted.size * 0.95).to_i]
percentile_95 * 2 # Double for safety margin
end
def record_acquisition(resource_id, duration)
@mutex.synchronize do
@lock_times[resource_id] << duration
@lock_times[resource_id].shift if @lock_times[resource_id].size > 100
end
end
end
Lock granularity mismatches create bottlenecks or excessive complexity. Single global lock eliminates deadlock but serializes all operations. Per-object locks increase parallelism but multiply deadlock scenarios. Finding the right granularity requires analyzing contention patterns and access frequencies.
Error Handling & Edge Cases
Deadlock recovery requires detecting the deadlock, selecting a victim thread, and aborting it. The aborted thread must roll back any partial changes to maintain consistency. Database systems implement this automatically, but application code must handle it explicitly.
class DeadlockRecovery
class Deadlock < StandardError; end
def transfer_with_recovery(from, to, amount, max_retries = 3)
retries = 0
begin
perform_transfer(from, to, amount)
rescue Deadlock => e
retries += 1
if retries < max_retries
backoff = Random.rand(0.1..0.5) * retries
sleep backoff # Exponential backoff
retry
else
raise "Transfer failed after #{max_retries} attempts"
end
end
end
def perform_transfer(from, to, amount)
acquired = []
timeout = 5
# Try to acquire both locks
start = Time.now
[from, to].each do |account|
loop do
if account.try_lock
acquired << account
break
end
if Time.now - start > timeout
# Deadlock detected
acquired.each(&:unlock)
raise Deadlock, "Could not acquire all locks"
end
sleep 0.01
end
end
begin
from.debit(amount)
to.credit(amount)
ensure
acquired.each(&:unlock)
end
end
end
Partial rollback handles cases where some operations in a transaction complete before deadlock occurs. The recovery mechanism must undo completed operations while preserving system invariants.
class TransactionalOperation
def initialize
@operations = []
@mutex = Mutex.new
end
def add_operation(&operation)
@mutex.synchronize do
@operations << operation
end
end
def execute_with_rollback
completed = []
begin
@operations.each do |operation|
result = operation.call
completed << result
end
rescue => e
# Rollback completed operations in reverse order
completed.reverse.each do |result|
result[:rollback].call if result[:rollback]
end
raise
end
end
end
# Usage example
transaction = TransactionalOperation.new
transaction.add_operation do
account1.lock
balance = account1.debit(100)
{
rollback: -> { account1.credit(100); account1.unlock }
}
end
transaction.add_operation do
account2.lock
account2.credit(100)
{
rollback: -> { account2.debit(100); account2.unlock }
}
end
transaction.execute_with_rollback
Thread interruption during critical sections can leave shared state inconsistent. Ruby's Thread#raise injects exceptions into threads, but this can corrupt data if the exception occurs mid-operation. The Thread.handle_interrupt method defers interrupts until safe points.
class InterruptSafeOperation
def critical_section
Thread.handle_interrupt(Exception => :never) do
begin
@mutex.synchronize do
# This section cannot be interrupted
perform_critical_update
end
ensure
Thread.handle_interrupt(Exception => :immediate) do
# Allow interrupts during cleanup
cleanup
end
end
end
end
def perform_critical_update
@data[:value] = calculate_new_value
@data[:timestamp] = Time.now
@data[:checksum] = calculate_checksum(@data[:value])
# All fields updated atomically - no partial state
end
end
Priority inversion occurs when a high-priority thread waits for a lock held by a low-priority thread. If medium-priority threads preempt the low-priority thread, the high-priority thread waits indefinitely. Priority inheritance protocols temporarily boost the low-priority thread's priority to match the waiting high-priority thread.
# Ruby doesn't directly support priority inheritance
# but we can implement a basic version
class PriorityInheritanceMutex
def initialize
@mutex = Mutex.new
@owner = nil
@original_priority = nil
end
def synchronize
requesting_priority = Thread.current.priority
@mutex.synchronize do
if @owner && @owner.priority < requesting_priority
# Boost owner's priority temporarily
@original_priority = @owner.priority
@owner.priority = requesting_priority
end
@owner = Thread.current
end
begin
yield
ensure
@mutex.synchronize do
if @original_priority
Thread.current.priority = @original_priority
@original_priority = nil
end
@owner = nil
end
end
end
end
Reference
Ruby Synchronization Classes
| Class | Purpose | Key Methods |
|---|---|---|
| Mutex | Basic mutual exclusion lock | synchronize, lock, unlock, try_lock, locked? |
| Monitor | Mutex with condition variables | synchronize, new_cond |
| ConditionVariable | Thread signaling within Monitor | wait, signal, broadcast |
| Queue | Thread-safe FIFO queue | push, pop, empty?, size |
| SizedQueue | Bounded thread-safe queue | push, pop, max=, num_waiting |
Deadlock Prevention Strategies
| Strategy | Mechanism | Advantages | Disadvantages |
|---|---|---|---|
| Resource Ordering | Acquire resources in fixed order | Simple, no runtime overhead | Requires global knowledge, constrains design |
| Lock Timeout | Abort if acquisition exceeds timeout | Detects deadlock quickly | False positives, requires retry logic |
| Try Lock | Non-blocking acquisition | No wait time | Requires polling or retry |
| All-or-Nothing | Acquire all resources atomically | Predictable behavior | Reduces concurrency |
| Two-Phase Locking | Separate acquisition and release phases | Works for transactions | May reduce parallelism |
| No Hold-and-Wait | Release all before acquiring new | Prevents circular wait | Forces restart of operations |
Coffman Conditions Checklist
| Condition | Description | Breaking Strategy |
|---|---|---|
| Mutual Exclusion | Resources not sharable | Make resources sharable (read-only data) |
| Hold and Wait | Holding resources while requesting more | Acquire all resources atomically |
| No Preemption | Cannot forcibly take resources | Allow resource preemption with rollback |
| Circular Wait | Circular chain of resource requests | Impose total ordering on resources |
Ruby Mutex Methods
| Method | Behavior | Return Value |
|---|---|---|
| lock | Blocks until lock acquired | self |
| unlock | Releases lock, raises error if not owner | self |
| try_lock | Attempts non-blocking acquisition | true if acquired, false otherwise |
| locked? | Checks lock status | true if any thread holds lock |
| synchronize | Acquires lock, executes block, releases | Block return value |
| owned? | Checks if current thread owns lock | true if current thread owns lock |
Deadlock Detection Approaches
| Approach | How It Works | Use Case |
|---|---|---|
| Timeout | Abort if lock not acquired within time limit | Applications with predictable lock hold times |
| Resource Graph | Build wait-for graph, detect cycles | Systems with complex resource dependencies |
| Wait-Die | Older transactions wait, younger abort | Database systems with timestamps |
| Wound-Wait | Older transactions preempt, younger wait | Database systems favoring older transactions |
| Periodic Check | Scan for waiting threads at intervals | Background deadlock detection |
Common Lock Patterns
| Pattern | Code Structure | Deadlock Safe |
|---|---|---|
| Simple Lock | mutex.synchronize { operation } | If single lock |
| Nested Locks | lock_a.synchronize { lock_b.synchronize { } } | Only if ordered |
| Try Lock Loop | loop { break if try_lock; sleep } | Yes, but may livelock |
| Timeout Lock | Timeout.timeout(n) { lock } | Yes, with proper retry |
| Ordered Acquisition | locks.sort_by { order }.each { lock } | Yes |
Recovery Strategies
| Strategy | Mechanism | Overhead |
|---|---|---|
| Abort and Retry | Detect deadlock, abort one thread, retry | Low |
| Rollback | Undo partial changes, restart transaction | Medium |
| Checkpoint | Save state periodically, restore on failure | High |
| Resource Preemption | Forcibly take resources from thread | Medium |
| Transaction Restart | Abort entire transaction, restart from beginning | Low |