class ActiveSupport::Concurrency::ShareLock
A share/exclusive lock, otherwise known as a read/write lock.
Public Class Methods
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.
# File lib/active_support/concurrency/share_lock.rb, line 17 def initialize super() @cv = new_cond @sharing = Hash.new(0) @waiting = {} @exclusive_thread = nil @exclusive_depth = 0 end
Public Instance Methods
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.
See start_exclusive
for other options.
# File lib/active_support/concurrency/share_lock.rb, line 114 def exclusive(purpose: nil, compatible: [], after_compatible: [], no_wait: false) if start_exclusive(purpose: purpose, compatible: compatible, no_wait: no_wait) begin yield ensure stop_exclusive(compatible: after_compatible) end end end
Execute the supplied block while holding the Share lock.
# File lib/active_support/concurrency/share_lock.rb, line 125 def sharing start_sharing begin yield ensure stop_sharing end end
Returns false if no_wait
is set and the lock is not
immediately available. Otherwise, returns true after the lock has been
acquired.
purpose
and compatible
work together; while this
thread is waiting for the exclusive lock, it will yield its share (if any)
to any other attempt whose purpose
appears in this
attempt's compatible
list. This allows a “loose” upgrade,
which, being less strict, 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. With purpose
matching, it is possible to yield only to other threads whose activity will
not interfere.
# File lib/active_support/concurrency/share_lock.rb, line 42 def start_exclusive(purpose: nil, compatible: [], no_wait: false) synchronize do unless @exclusive_thread == Thread.current if busy_for_exclusive?(purpose) return false if no_wait yield_shares(purpose: purpose, compatible: compatible, block_share: true) do @cv.wait_while { busy_for_exclusive?(purpose) } end end @exclusive_thread = Thread.current end @exclusive_depth += 1 true end end
# File lib/active_support/concurrency/share_lock.rb, line 80 def start_sharing synchronize do if @sharing[Thread.current] > 0 || @exclusive_thread == Thread.current # We already hold a lock; nothing to wait for elsif @waiting[Thread.current] # We're nested inside a +yield_shares+ call: we'll resume as # soon as there isn't an exclusive lock in our way @cv.wait_while { @exclusive_thread } else # This is an initial / outermost share call: any outstanding # requests for an exclusive lock get to go first @cv.wait_while { busy_for_sharing?(false) } end @sharing[Thread.current] += 1 end end
Relinquish the exclusive lock. Must only be called by the thread that called #start_exclusive (and currently holds the lock).
# File lib/active_support/concurrency/share_lock.rb, line 62 def stop_exclusive(compatible: []) synchronize do raise "invalid unlock" if @exclusive_thread != Thread.current @exclusive_depth -= 1 if @exclusive_depth == 0 @exclusive_thread = nil if eligible_waiters?(compatible) yield_shares(compatible: compatible, block_share: true) do @cv.wait_while { @exclusive_thread || eligible_waiters?(compatible) } end end @cv.broadcast end end end
# File lib/active_support/concurrency/share_lock.rb, line 97 def stop_sharing synchronize do if @sharing[Thread.current] > 1 @sharing[Thread.current] -= 1 else @sharing.delete Thread.current @cv.broadcast end end end
Private Instance Methods
Must be called within synchronize
# File lib/active_support/concurrency/share_lock.rb, line 171 def busy_for_exclusive?(purpose) busy_for_sharing?(purpose) || @sharing.size > (@sharing[Thread.current] > 0 ? 1 : 0) end
# File lib/active_support/concurrency/share_lock.rb, line 176 def busy_for_sharing?(purpose) (@exclusive_thread && @exclusive_thread != Thread.current) || @waiting.any? { |t, (_, c)| t != Thread.current && !c.include?(purpose) } end
# File lib/active_support/concurrency/share_lock.rb, line 181 def eligible_waiters?(compatible) @waiting.any? { |t, (p, _)| compatible.include?(p) && @waiting.all? { |t2, (_, c2)| t == t2 || c2.include?(p) } } end