blob: f1c6230084823b42d72e67bc90683eb06b804a0b (
plain) (
tree)
|
|
require 'thread'
require 'monitor'
module ActiveSupport
module Concurrency
# A share/exclusive lock, otherwise known as a read/write lock.
#
# https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock
#--
# Note that a pending Exclusive lock attempt does not block incoming
# Share requests (i.e., we are "read-preferring"). That seems
# consistent with the behavior of +loose_upgrades+, but may be the
# wrong choice otherwise: it nominally reduces the possibility of
# deadlock by risking starvation instead.
class ShareLock
include MonitorMixin
# We track Thread objects, instead of just using counters, because
# we need exclusive locks to be reentrant, and we need to be able
# to upgrade share locks to exclusive.
# If +loose_upgrades+ is false (the default), then a thread that
# is waiting on an Exclusive lock will continue to hold any Share
# lock that it has already established. This is safer, but can
# lead to deadlock.
#
# If +loose_upgrades+ is true, a thread waiting on an Exclusive
# lock will temporarily relinquish its Share lock. Being less
# strict, this behavior prevents some classes of deadlocks. For
# many resources, loose upgrades are sufficient: if a thread is
# awaiting a lock, it is not running any other code.
attr_reader :loose_upgrades
def initialize(loose_upgrades = false)
@loose_upgrades = loose_upgrades
super()
@cv = new_cond
@sharing = Hash.new(0)
@exclusive_thread = nil
@exclusive_depth = 0
end
# Returns false if +no_wait+ is specified and the lock is not
# immediately available. Otherwise, returns true after the lock
# has been acquired.
def start_exclusive(no_wait=false)
synchronize do
unless @exclusive_thread == Thread.current
return false if no_wait && busy?
loose_shares = nil
if @loose_upgrades
loose_shares = @sharing.delete(Thread.current)
end
@cv.wait_while { busy? } if busy?
@exclusive_thread = Thread.current
@sharing[Thread.current] = loose_shares if loose_shares
end
@exclusive_depth += 1
true
end
end
# Relinquish the exclusive lock. Must only be called by the thread
# that called start_exclusive (and currently holds the lock).
def stop_exclusive
synchronize do
raise "invalid unlock" if @exclusive_thread != Thread.current
@exclusive_depth -= 1
if @exclusive_depth == 0
@exclusive_thread = nil
@cv.broadcast
end
end
end
def start_sharing
synchronize do
if @exclusive_thread && @exclusive_thread != Thread.current
@cv.wait_while { @exclusive_thread }
end
@sharing[Thread.current] += 1
end
end
def stop_sharing
synchronize do
if @sharing[Thread.current] > 1
@sharing[Thread.current] -= 1
else
@sharing.delete Thread.current
@cv.broadcast
end
end
end
# Execute the supplied block while holding the Exclusive lock. If
# +no_wait+ is set and the lock is not immediately available,
# returns +nil+ without yielding. Otherwise, returns the result of
# the block.
def exclusive(no_wait=false)
if start_exclusive(no_wait)
begin
yield
ensure
stop_exclusive
end
end
end
# Execute the supplied block while holding the Share lock.
def sharing
start_sharing
begin
yield
ensure
stop_sharing
end
end
private
# Must be called within synchronize
def busy?
(@exclusive_thread && @exclusive_thread != Thread.current) ||
@sharing.size > (@sharing[Thread.current] > 0 ? 1 : 0)
end
end
end
end
|