CrackedRuby CrackedRuby

Overview

A critical section represents a segment of code that accesses shared resources and must execute atomically without interruption from concurrent threads or processes. When multiple threads attempt to modify shared data simultaneously, the absence of proper synchronization leads to race conditions, data corruption, and unpredictable program behavior.

The critical section problem emerges directly from concurrent execution environments where multiple execution contexts share memory or resources. Operating systems, multi-threaded applications, and distributed systems all face this fundamental challenge: ensuring that operations on shared state complete without interference.

Consider a bank account balance update scenario:

class BankAccount
  attr_reader :balance
  
  def initialize(initial_balance)
    @balance = initial_balance
  end
  
  def withdraw(amount)
    # Critical section begins
    current = @balance
    sleep(0.001)  # Simulate processing time
    @balance = current - amount
    # Critical section ends
  end
end

account = BankAccount.new(1000)

threads = 10.times.map do
  Thread.new { account.withdraw(100) }
end

threads.each(&:join)
puts account.balance  # Expected: 0, Actual: unpredictable (often > 0)

Without synchronization, concurrent withdrawals produce incorrect balances. The read-modify-write sequence forms a critical section requiring atomic execution.

Critical sections appear throughout software systems: database transaction processing, file system operations, cache updates, session management, and resource allocation. Any shared mutable state accessed by concurrent execution contexts requires critical section protection.

The challenge intensifies in modern computing environments with multi-core processors executing truly parallel threads, distributed systems coordinating across network boundaries, and high-throughput services handling thousands of concurrent requests.

Key Principles

Critical sections operate under three fundamental requirements that define correct concurrent behavior: mutual exclusion, progress, and bounded waiting. These properties ensure both safety and liveness in concurrent systems.

Mutual exclusion mandates that at most one thread executes within a critical section at any given moment. When thread A holds the lock and executes the critical section, thread B must wait. This exclusivity prevents simultaneous access to shared resources that could corrupt data.

Progress requires that if no thread executes in the critical section, one waiting thread must eventually enter. The system cannot deadlock in a state where all threads wait indefinitely despite resource availability. Selection of the next thread to enter must occur in finite time.

Bounded waiting guarantees that once a thread requests entry to a critical section, other threads can enter ahead of it only a limited number of times. This property prevents starvation where a thread perpetually waits while others repeatedly enter.

A critical section consists of four logical regions:

entry section    → acquire synchronization
critical section → access shared resource
exit section     → release synchronization
remainder section → non-critical code

The entry section implements the protocol for requesting permission to enter. This typically involves acquiring a lock, checking a semaphore, or executing a more complex synchronization algorithm. The entry section may block if another thread currently occupies the critical section.

The critical section itself contains the actual operations on shared resources. Code within this region assumes exclusive access and performs reads and writes without additional synchronization.

The exit section releases the synchronization primitive, allowing waiting threads to proceed. Proper exit section implementation is critical—failing to release locks causes deadlock.

The remainder section executes code that does not require synchronization. Well-designed concurrent programs minimize time spent in critical sections and maximize work in remainder sections.

Atomicity represents the core abstraction critical sections provide. From an external observer's perspective, the entire critical section appears to execute instantaneously and indivisibly. Even though the critical section comprises multiple instructions executed over time, no other thread observes intermediate states.

Visibility ensures that changes made within a critical section become visible to other threads. Modern processors and compilers reorder instructions and cache values, potentially hiding updates. Synchronization primitives include memory barriers that force visibility of shared variable changes.

The granularity of critical sections significantly impacts performance and correctness. Fine-grained locking protects small, specific data structures, enabling higher concurrency but increasing complexity. Coarse-grained locking protects larger regions with simpler reasoning but reduces parallelism. The balance between lock granularity and contention defines concurrent system scalability.

Ruby Implementation

Ruby provides several synchronization primitives for implementing critical sections, with different characteristics suitable for various scenarios. The Global Interpreter Lock (GIL) in MRI Ruby serializes bytecode execution, but critical sections remain necessary because the GIL releases during blocking I/O and C extensions, and other Ruby implementations like JRuby lack a GIL entirely.

The Mutex class implements the fundamental mutual exclusion primitive:

require 'thread'

class Counter
  def initialize
    @count = 0
    @mutex = Mutex.new
  end
  
  def increment
    @mutex.synchronize do
      # Critical section
      current = @count
      sleep(0.001)  # Simulate processing
      @count = current + 1
    end
  end
  
  def value
    @mutex.synchronize { @count }
  end
end

counter = Counter.new
threads = 100.times.map do
  Thread.new { 10.times { counter.increment } }
end
threads.each(&:join)
puts counter.value  # => 1000 (consistent)

The synchronize method acquires the mutex, executes the block, and releases the mutex even if exceptions occur. Manual lock management using lock and unlock requires careful exception handling:

def risky_operation
  @mutex.lock
  begin
    # Critical section that might raise
    perform_operation
  ensure
    @mutex.unlock  # Always release, even on exception
  end
end

The Monitor module extends mutex functionality with condition variables and reentrant locking:

require 'monitor'

class BoundedQueue
  def initialize(capacity)
    @items = []
    @capacity = capacity
    @monitor = Monitor.new
    @not_full = @monitor.new_cond
    @not_empty = @monitor.new_cond
  end
  
  def put(item)
    @monitor.synchronize do
      while @items.size >= @capacity
        @not_full.wait  # Release lock and wait
      end
      @items << item
      @not_empty.signal  # Wake waiting consumer
    end
  end
  
  def take
    @monitor.synchronize do
      while @items.empty?
        @not_empty.wait
      end
      item = @items.shift
      @not_full.signal
      item
    end
  end
end

Monitors support reentrant locking where the same thread can acquire the lock multiple times:

class ReentrantExample
  def initialize
    @monitor = Monitor.new
  end
  
  def outer_method
    @monitor.synchronize do
      puts "Outer method holds lock"
      inner_method  # Can acquire lock again
    end
  end
  
  def inner_method
    @monitor.synchronize do
      puts "Inner method also holds lock"
    end
  end
end

Thread-local storage provides an alternative to shared state requiring synchronization:

class RequestContext
  def self.current
    Thread.current[:request_context]
  end
  
  def self.current=(context)
    Thread.current[:request_context] = context
  end
end

# Each thread has independent context
def process_request(id)
  RequestContext.current = { request_id: id, timestamp: Time.now }
  # No synchronization needed for thread-local data
  perform_work
end

The Queue class in Ruby's standard library provides thread-safe queue operations without explicit synchronization:

require 'thread'

queue = Queue.new

producer = Thread.new do
  10.times do |i|
    queue << i
    sleep(0.1)
  end
end

consumer = Thread.new do
  10.times do
    item = queue.pop  # Blocks until item available
    puts "Processed: #{item}"
  end
end

producer.join
consumer.join

Atomic operations using concurrent-ruby gem provide lock-free synchronization for simple operations:

require 'concurrent'

class AtomicCounter
  def initialize
    @count = Concurrent::AtomicFixnum.new(0)
  end
  
  def increment
    @count.increment
  end
  
  def value
    @count.value
  end
end

The Concurrent::Map provides thread-safe hash operations without manual locking:

require 'concurrent'

class Cache
  def initialize
    @cache = Concurrent::Map.new
  end
  
  def fetch(key)
    @cache.fetch_or_store(key) do
      # Expensive computation runs once per key
      expensive_operation(key)
    end
  end
end

Ruby's fiber-based concurrency using Async library requires different synchronization approaches:

require 'async'
require 'async/barrier'

class AsyncCriticalSection
  def initialize
    @semaphore = Async::Semaphore.new(1)
  end
  
  def synchronized
    @semaphore.acquire do
      yield
    end
  end
end

Implementation Approaches

Different synchronization strategies suit different scenarios, each with distinct performance characteristics and complexity trade-offs.

Lock-based synchronization uses explicit locks (mutexes, semaphores) to guard critical sections. This approach provides straightforward reasoning about correctness but introduces potential deadlock and priority inversion problems. Lock-based designs scale well for low contention scenarios but degrade when many threads compete for the same lock.

Implementation strategy:

class LockBasedCache
  def initialize
    @data = {}
    @lock = Mutex.new
  end
  
  def get(key)
    @lock.synchronize { @data[key] }
  end
  
  def put(key, value)
    @lock.synchronize { @data[key] = value }
  end
end

Lock-free synchronization uses atomic compare-and-swap operations to modify shared data without locks. This approach eliminates deadlock possibilities and reduces context switching overhead. Lock-free algorithms require careful design to handle ABA problems and ensure progress guarantees.

Implementation strategy using atomic operations:

require 'concurrent'

class LockFreeStack
  def initialize
    @head = Concurrent::AtomicReference.new(nil)
  end
  
  def push(value)
    loop do
      current_head = @head.get
      new_node = { value: value, next: current_head }
      return if @head.compare_and_set(current_head, new_node)
      # Retry if another thread modified head
    end
  end
  
  def pop
    loop do
      current_head = @head.get
      return nil if current_head.nil?
      new_head = current_head[:next]
      if @head.compare_and_set(current_head, new_head)
        return current_head[:value]
      end
      # Retry if another thread modified head
    end
  end
end

Read-write locks optimize for scenarios with frequent reads and infrequent writes. Multiple readers can acquire the lock simultaneously, but writers require exclusive access. This pattern dramatically improves throughput for read-heavy workloads.

Implementation strategy:

require 'concurrent'

class ReadWriteCache
  def initialize
    @data = {}
    @lock = Concurrent::ReadWriteLock.new
  end
  
  def get(key)
    @lock.with_read_lock { @data[key] }
  end
  
  def put(key, value)
    @lock.with_write_lock { @data[key] = value }
  end
  
  def keys
    @lock.with_read_lock { @data.keys }
  end
end

Optimistic locking assumes conflicts rarely occur and validates assumptions before committing changes. This approach works well for distributed systems and database transactions where the cost of coordination exceeds the cost of occasional retries.

Implementation strategy:

class OptimisticCounter
  def initialize
    @value = 0
    @version = 0
    @lock = Mutex.new
  end
  
  def increment
    loop do
      # Read without lock
      current_value = @value
      current_version = @version
      
      # Perform computation
      new_value = current_value + 1
      
      # Validate and commit
      @lock.synchronize do
        if @version == current_version
          @value = new_value
          @version += 1
          return new_value
        end
        # Retry if version changed
      end
    end
  end
end

Fine-grained locking uses multiple locks to protect different portions of a data structure. This approach increases concurrency by allowing threads to operate on different sections simultaneously but adds complexity in lock ordering to prevent deadlock.

Implementation strategy:

class StripedHashMap
  STRIPE_COUNT = 16
  
  def initialize
    @stripes = Array.new(STRIPE_COUNT) { { data: {}, lock: Mutex.new } }
  end
  
  def stripe_for(key)
    @stripes[key.hash % STRIPE_COUNT]
  end
  
  def get(key)
    stripe = stripe_for(key)
    stripe[:lock].synchronize { stripe[:data][key] }
  end
  
  def put(key, value)
    stripe = stripe_for(key)
    stripe[:lock].synchronize { stripe[:data][key] = value }
  end
end

Message passing eliminates shared state by having threads communicate through message queues. Each thread owns its data exclusively, removing the need for locks. This approach simplifies reasoning about concurrency but introduces queuing overhead.

Implementation strategy:

class ActorCounter
  def initialize
    @count = 0
    @queue = Queue.new
    @thread = Thread.new { run }
  end
  
  def increment
    @queue << :increment
  end
  
  def value
    result = Queue.new
    @queue << [:get, result]
    result.pop
  end
  
  private
  
  def run
    loop do
      msg = @queue.pop
      case msg
      when :increment
        @count += 1
      when Array
        command, result_queue = msg
        result_queue << @count if command == :get
      end
    end
  end
end

Common Patterns

Several patterns address recurring critical section challenges, each providing tested solutions to specific concurrency problems.

Double-checked locking optimizes lazy initialization by checking conditions before acquiring locks:

class Singleton
  def self.instance
    return @instance if @instance  # First check without lock
    
    @lock ||= Mutex.new
    @lock.synchronize do
      @instance ||= new  # Second check with lock
    end
    @instance
  end
  
  private_class_method :new
end

This pattern reduces lock contention for initialized objects but requires memory barrier guarantees. Ruby's instance variable semantics provide sufficient guarantees for this pattern.

Thread-safe lazy initialization ensures resource creation occurs exactly once:

class ExpensiveResource
  def self.instance
    @instance ||= begin
      @mutex ||= Mutex.new
      @mutex.synchronize { @instance ||= new }
    end
  end
  
  def initialize
    @connection = establish_connection
    @cache = build_cache
  end
end

Lock ordering prevents deadlock by establishing a global order for lock acquisition:

class TransferSystem
  def transfer(from_account, to_account, amount)
    # Always lock accounts in consistent order by ID
    first, second = [from_account, to_account].sort_by(&:id)
    
    first.lock.synchronize do
      second.lock.synchronize do
        from_account.withdraw(amount)
        to_account.deposit(amount)
      end
    end
  end
end

Reader-writer pattern allows multiple concurrent readers with exclusive writer access:

class ConfigurationStore
  def initialize
    @config = {}
    @readers = 0
    @writer = false
    @mutex = Mutex.new
    @read_cond = ConditionVariable.new
    @write_cond = ConditionVariable.new
  end
  
  def read
    @mutex.synchronize do
      @read_cond.wait(@mutex) while @writer
      @readers += 1
    end
    
    begin
      yield @config
    ensure
      @mutex.synchronize do
        @readers -= 1
        @write_cond.signal if @readers == 0
      end
    end
  end
  
  def write
    @mutex.synchronize do
      @write_cond.wait(@mutex) while @readers > 0 || @writer
      @writer = true
    end
    
    begin
      yield @config
    ensure
      @mutex.synchronize do
        @writer = false
        @read_cond.broadcast
        @write_cond.signal
      end
    end
  end
end

Scoped locking uses Ruby blocks to ensure lock release even during exceptions:

class Resource
  def initialize
    @mutex = Mutex.new
  end
  
  def with_lock
    @mutex.synchronize { yield }
  end
  
  def perform_operation
    with_lock do
      # Lock automatically released even if exception occurs
      risky_operation
    end
  end
end

Try-lock pattern attempts lock acquisition with timeout to avoid indefinite blocking:

class TimeoutExample
  def initialize
    @mutex = Mutex.new
  end
  
  def try_operation(timeout = 5)
    acquired = false
    deadline = Time.now + timeout
    
    until acquired || Time.now > deadline
      acquired = @mutex.try_lock
      sleep(0.01) unless acquired
    end
    
    if acquired
      begin
        yield
      ensure
        @mutex.unlock
      end
    else
      raise TimeoutError, "Could not acquire lock"
    end
  end
end

Copy-on-write pattern avoids locks for read-heavy scenarios by creating new copies for modifications:

class CopyOnWriteList
  def initialize
    @items = []
    @mutex = Mutex.new
  end
  
  def add(item)
    @mutex.synchronize do
      @items = @items + [item]  # Create new array
    end
  end
  
  def each
    snapshot = @items  # Safe to iterate without lock
    snapshot.each { |item| yield item }
  end
end

Error Handling & Edge Cases

Critical sections introduce failure modes beyond typical programming errors, requiring specific detection and recovery strategies.

Deadlock occurs when threads wait circularly for locks held by each other:

# Deadlock scenario
class DeadlockExample
  def initialize
    @lock_a = Mutex.new
    @lock_b = Mutex.new
  end
  
  def operation_1
    @lock_a.synchronize do
      sleep(0.01)
      @lock_b.synchronize do
        # Work
      end
    end
  end
  
  def operation_2
    @lock_b.synchronize do
      sleep(0.01)
      @lock_a.synchronize do
        # Work
      end
    end
  end
end

# If thread 1 calls operation_1 and thread 2 calls operation_2,
# deadlock occurs

Prevention strategies include lock ordering, timeout-based acquisition, and deadlock detection algorithms. Consistent lock ordering eliminates circular wait conditions:

class DeadlockPrevention
  def initialize
    @lock_a = Mutex.new
    @lock_b = Mutex.new
  end
  
  def acquire_locks
    # Always acquire in consistent order
    @lock_a.synchronize do
      @lock_b.synchronize do
        yield
      end
    end
  end
end

Race conditions manifest when timing determines program correctness:

class RaceCondition
  def initialize
    @value = 0
  end
  
  def unsafe_increment
    temp = @value
    # Context switch can occur here
    @value = temp + 1
  end
end

# Multiple threads calling unsafe_increment produce incorrect results

Detection requires careful code review, stress testing, and tools like thread sanitizers. Prevention requires identifying all shared mutable state and protecting it appropriately.

Priority inversion occurs when high-priority threads wait for locks held by low-priority threads:

# Thread priorities affect scheduling but not lock acquisition
high_priority = Thread.new do
  Thread.current.priority = 10
  @shared_lock.synchronize do
    # High priority thread blocked if low priority holds lock
  end
end

low_priority = Thread.new do
  Thread.current.priority = -10
  @shared_lock.synchronize do
    sleep(1)  # Holds lock while low priority
  end
end

Priority inheritance protocols mitigate this problem by temporarily elevating the priority of lock holders, though Ruby does not implement this automatically.

Lost updates happen when concurrent modifications overwrite each other:

class LostUpdate
  def initialize
    @balance = 1000
  end
  
  def withdraw(amount)
    current = @balance
    # Another thread may modify @balance here
    @balance = current - amount
  end
end

# Two concurrent $100 withdrawals may result in $900 instead of $800

Protection requires atomic read-modify-write operations:

def safe_withdraw(amount)
  @mutex.synchronize do
    @balance -= amount
  end
end

Livelock occurs when threads repeatedly respond to each other without making progress:

class LivelockExample
  def polite_acquire(resource, other_resource)
    loop do
      acquired = resource.try_lock
      if acquired
        if other_resource.locked?
          resource.unlock  # Be polite
          sleep(0.001)
        else
          return
        end
      end
    end
  end
end

Both threads perpetually yield to each other, making no progress. Randomized backoff or timeout mechanisms break the cycle.

Starvation happens when threads indefinitely wait for resource access:

class StarvationRisk
  def initialize
    @lock = Mutex.new
  end
  
  def high_frequency_operation
    loop do
      @lock.synchronize do
        # Fast operation
      end
    end
  end
  
  def low_frequency_operation
    @lock.synchronize do
      # May never acquire lock if high_frequency_operation runs constantly
    end
  end
end

Fair locks, priority queues, or aging mechanisms ensure all threads eventually gain access.

Exception safety requires careful lock release even during errors:

class ExceptionSafe
  def operation
    @mutex.lock
    begin
      risky_operation  # May raise exception
    ensure
      @mutex.unlock  # Always release lock
    end
  end
  
  # Better: use synchronize which handles this automatically
  def better_operation
    @mutex.synchronize do
      risky_operation
    end
  end
end

Memory visibility issues arise when changes made by one thread remain invisible to others:

class VisibilityProblem
  def initialize
    @ready = false
    @data = nil
  end
  
  def writer
    @data = expensive_computation
    @ready = true  # May not be visible to reader immediately
  end
  
  def reader
    while !@ready
      # May loop forever due to visibility issues
    end
    use(@data)  # @data may still be nil
  end
end

Proper synchronization enforces memory barriers that guarantee visibility:

def writer
  @mutex.synchronize do
    @data = expensive_computation
    @ready = true
  end
end

def reader
  @mutex.synchronize do
    return unless @ready
  end
  use(@data)  # Guaranteed to see latest @data
end

Performance Considerations

Critical sections directly impact system throughput, latency, and scalability. Lock contention creates performance bottlenecks that worsen with core count and concurrent load.

Lock contention occurs when multiple threads compete for the same lock, forcing serialization of parallel work. High contention degrades performance below single-threaded execution due to context switching overhead:

require 'benchmark'

class ContentionTest
  def initialize
    @counter = 0
    @mutex = Mutex.new
  end
  
  def high_contention
    1000.times do
      @mutex.synchronize { @counter += 1 }
    end
  end
  
  def low_contention
    local_sum = 0
    1000.times { local_sum += 1 }
    @mutex.synchronize { @counter += local_sum }
  end
end

# Benchmark shows low_contention significantly faster with multiple threads

Reducing critical section size minimizes contention. Move computation outside locks:

def inefficient
  @mutex.synchronize do
    result = expensive_computation(data)
    @cache[key] = result
  end
end

def efficient
  result = expensive_computation(data)
  @mutex.synchronize do
    @cache[key] = result
  end
end

Lock granularity determines the balance between concurrency and complexity. Coarse-grained locks simplify reasoning but limit parallelism:

class CoarseGrained
  def initialize
    @data = {}
    @lock = Mutex.new
  end
  
  def update_all(updates)
    @lock.synchronize do
      updates.each { |key, value| @data[key] = value }
    end
  end
end

Fine-grained locks increase concurrency but introduce deadlock risk and overhead:

class FineGrained
  def initialize
    @data = {}
    @locks = Hash.new { |h, k| h[k] = Mutex.new }
  end
  
  def update(key, value)
    @locks[key].synchronize do
      @data[key] = value
    end
  end
end

Lock-free algorithms eliminate blocking entirely, achieving better scalability under contention:

require 'benchmark'
require 'concurrent'

class LockBasedCounter
  def initialize
    @count = 0
    @mutex = Mutex.new
  end
  
  def increment
    @mutex.synchronize { @count += 1 }
  end
end

class LockFreeCounter
  def initialize
    @count = Concurrent::AtomicFixnum.new(0)
  end
  
  def increment
    @count.increment
  end
end

# Lock-free version scales better with thread count

Context switching overhead increases with lock wait time. Each blocked thread consumes stack space and incurs switching costs when scheduled:

# Holding locks across blocking I/O increases contention
def inefficient_io
  @mutex.synchronize do
    data = blocking_network_call  # Holds lock during I/O
    @cache[key] = data
  end
end

def efficient_io
  data = blocking_network_call
  @mutex.synchronize do
    @cache[key] = data  # Lock held only for cache update
  end
end

Thundering herd problems occur when many threads wake simultaneously and compete for a lock:

class ThunderingHerd
  def initialize
    @mutex = Mutex.new
    @condition = ConditionVariable.new
  end
  
  def wait_for_event
    @mutex.synchronize do
      @condition.wait(@mutex)
      # All threads wake when signaled
    end
  end
  
  def trigger_event
    @mutex.synchronize do
      @condition.broadcast  # Wakes all waiting threads
    end
  end
end

Selective notification reduces contention:

def trigger_single
  @mutex.synchronize do
    @condition.signal  # Wakes only one thread
  end
end

Spin locks trade CPU cycles for reduced latency when critical sections execute quickly:

class SpinLock
  def initialize
    @locked = Concurrent::AtomicBoolean.new(false)
  end
  
  def synchronize
    # Busy-wait instead of sleeping
    until @locked.compare_and_set(false, true)
      # Spin
    end
    begin
      yield
    ensure
      @locked.make_false
    end
  end
end

Spin locks waste CPU but avoid context switch overhead. Use only for critical sections shorter than context switch time (typically microseconds).

Reader-writer locks optimize read-heavy workloads by allowing concurrent readers:

require 'benchmark'

# Benchmark comparing Mutex vs ReadWriteLock for 90% reads, 10% writes
# ReadWriteLock shows 5-10x improvement for read-heavy patterns

Lock striping partitions data structures to reduce contention:

class StripedMap
  STRIPE_COUNT = 16
  
  def initialize
    @stripes = Array.new(STRIPE_COUNT) do
      { data: {}, lock: Mutex.new }
    end
  end
  
  def stripe_for(key)
    @stripes[key.hash % STRIPE_COUNT]
  end
  
  def get(key)
    stripe = stripe_for(key)
    stripe[:lock].synchronize { stripe[:data][key] }
  end
end

# Supports STRIPE_COUNT concurrent operations on different keys

Reference

Synchronization Primitives

Primitive Use Case Characteristics
Mutex Basic mutual exclusion Non-reentrant, blocking
Monitor Reentrant locking with conditions Reentrant, condition variables
Queue Thread-safe queue operations Blocking, built-in synchronization
ConditionVariable Thread coordination Requires mutex, supports wait/signal
AtomicFixnum Lock-free counter operations Non-blocking, limited operations
ReadWriteLock Read-heavy workloads Multiple readers, single writer
Semaphore Resource counting Permits multiple concurrent access

Critical Section Properties

Property Definition Violation Consequence
Mutual Exclusion At most one thread in critical section Data corruption, race conditions
Progress Waiting threads eventually enter Deadlock, system hang
Bounded Waiting Finite wait time after request Starvation, unfairness
Atomicity Operations appear indivisible Inconsistent state observation
Visibility Changes visible to other threads Stale data, incorrect behavior

Common Concurrency Problems

Problem Cause Detection Prevention
Deadlock Circular lock waiting Thread dumps, timeouts Lock ordering, deadlock detection
Race Condition Unsynchronized shared access Non-deterministic failures Proper synchronization
Livelock Repeated yielding without progress High CPU, no progress Randomized backoff, timeouts
Starvation Unfair resource allocation Thread never completes Fair locks, priority aging
Priority Inversion Low priority blocks high priority Unexpected delays Priority inheritance
Lost Update Concurrent overwrites Incorrect final state Atomic operations, locking

Ruby Mutex Operations

Method Behavior Exception Safety
synchronize Execute block with lock Automatic unlock on exception
lock Acquire lock Manual unlock required
unlock Release lock Raises if not owner
try_lock Non-blocking acquire Returns true if acquired
locked? Check lock status Does not wait
owned? Check ownership Returns true if current thread owns

Performance Characteristics

Pattern Throughput Latency Complexity Best For
Coarse-grained lock Low under contention Low Simple Low concurrency
Fine-grained lock High Medium Complex High concurrency
Lock-free Very high Very low Very complex High contention
Read-write lock High for reads Medium Medium Read-heavy
Message passing Medium High Medium Actor model
Optimistic lock High Low average Medium Rare conflicts

Lock Ordering Example

class Account
  attr_reader :id, :balance
  attr_accessor :lock
  
  def initialize(id, balance)
    @id = id
    @balance = balance
    @lock = Mutex.new
  end
end

def transfer(from, to, amount)
  first, second = [from, to].sort_by(&:id)
  
  first.lock.synchronize do
    second.lock.synchronize do
      from.balance -= amount
      to.balance += amount
    end
  end
end

Condition Variable Patterns

# Wait pattern
@mutex.synchronize do
  while !condition_met?
    @condition.wait(@mutex)
  end
  process
end

# Signal pattern
@mutex.synchronize do
  update_state
  @condition.signal      # Wake one
  # or
  @condition.broadcast   # Wake all
end

Atomic Operation Comparison

Operation Atomic Lock-Free Use Case
Read variable No N/A Requires synchronization
Write variable No N/A Requires synchronization
Increment counter With AtomicFixnum Yes Concurrent counting
Compare-and-swap Yes Yes Lock-free algorithms
Array append No No Requires locking
Hash update No No Requires locking or concurrent map

Memory Visibility Rules

Scenario Visibility Guaranteed Requires Synchronization
Within single thread Yes No
After synchronize block Yes Yes (implicit)
Thread.new parameter passing Yes No (implicit)
Thread.join return value Yes No (implicit)
Unsynchronized shared variable No Yes
Final fields after construction Yes No

Deadlock Prevention Strategies

Strategy Mechanism Trade-off
Lock ordering Acquire locks in consistent order Requires global knowledge
Lock timeout Abort after timeout May waste work
No hold and wait Acquire all locks atomically Reduces concurrency
Preemption Force lock release Complex recovery
Deadlock detection Detect and recover Runtime overhead