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 |