CrackedRuby CrackedRuby

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