Class: ActiveSupport::Concurrency::ShareLock
| Relationships & Source Files | |
| Super Chains via Extension / Inclusion / Inheritance | |
|
Instance Chain:
self,
MonitorMixin
|
|
| Inherits: | Object |
| Defined in: | activesupport/lib/active_support/concurrency/share_lock.rb |
Overview
A share/exclusive lock, otherwise known as a read/write lock.
Class Method Summary
- .new ⇒ ShareLock constructor
Instance Method Summary
-
#exclusive(purpose: nil, compatible: [], after_compatible: [], no_wait: false)
Execute the supplied block while holding the Exclusive lock.
-
#sharing
Execute the supplied block while holding the Share lock.
-
#start_exclusive(purpose: nil, compatible: [], no_wait: false)
Returns false if
no_waitis set and the lock is not immediately available. - #start_sharing
-
#stop_exclusive(compatible: [])
Relinquish the exclusive lock.
- #stop_sharing
-
#yield_shares(purpose: nil, compatible: [], block_share: false)
Temporarily give up all held Share locks while executing the supplied block, allowing any
compatibleexclusive lock request to proceed. -
#busy_for_exclusive?(purpose) ⇒ Boolean
private
Must be called within synchronize.
- #busy_for_sharing?(purpose) ⇒ Boolean private
- #current_owner private
- #eligible_waiters?(compatible) ⇒ Boolean private
- #wait_for(method, &block) private
-
#raw_state
Internal use only
We track execution-context objects (Threads or Fibers, depending on IsolatedExecutionState.isolation_level), 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.
Constructor Details
.new ⇒ ShareLock
# File 'activesupport/lib/active_support/concurrency/share_lock.rb', line 51
def initialize super() @cv = new_cond @sharing = Hash.new(0) @waiting = {} @sleeping = {} @exclusive_owner = nil @exclusive_depth = 0 end
Instance Method Details
#busy_for_exclusive?(purpose) ⇒ Boolean (private)
Must be called within synchronize
# File 'activesupport/lib/active_support/concurrency/share_lock.rb', line 212
def busy_for_exclusive?(purpose) busy_for_sharing?(purpose) || @sharing.size > (@sharing[current_owner] > 0 ? 1 : 0) end
#busy_for_sharing?(purpose) ⇒ Boolean (private)
# File 'activesupport/lib/active_support/concurrency/share_lock.rb', line 217
def busy_for_sharing?(purpose) owner = current_owner (@exclusive_owner && @exclusive_owner != owner) || @waiting.any? { |o, (_, c)| o != owner && !c.include?(purpose) } end
#current_owner (private)
[ GitHub ]# File 'activesupport/lib/active_support/concurrency/share_lock.rb', line 207
def current_owner ActiveSupport::IsolatedExecutionState.context end
#eligible_waiters?(compatible) ⇒ Boolean (private)
# File 'activesupport/lib/active_support/concurrency/share_lock.rb', line 223
def eligible_waiters?(compatible) @waiting.any? { |o, (p, _)| compatible.include?(p) && @waiting.all? { |o2, (_, c2)| o == o2 || c2.include?(p) } } end
#exclusive(purpose: nil, compatible: [], after_compatible: [], no_wait: false)
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 'activesupport/lib/active_support/concurrency/share_lock.rb', line 151
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
#raw_state
We track execution-context objects (Threads or Fibers, depending on IsolatedExecutionState.isolation_level), 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 'activesupport/lib/active_support/concurrency/share_lock.rb', line 19
def raw_state # :nodoc: synchronize do owners = @sleeping.keys | @sharing.keys | @waiting.keys owners |= [@exclusive_owner] if @exclusive_owner data = {} owners.each do |owner| purpose, compatible = @waiting[owner] data[owner] = { owner: owner, sharing: @sharing[owner], exclusive: @exclusive_owner == owner, purpose: purpose, compatible: compatible, waiting: !!@waiting[owner], sleeper: @sleeping[owner], } end # NB: Yields while holding our *internal* synchronize lock, # which is supposed to be used only for a few instructions at # a time. This allows the caller to inspect additional state # without things changing out from underneath, but would have # disastrous effects upon normal operation. Fortunately, this # method is only intended to be called when things have # already gone wrong. yield data end end
#sharing
Execute the supplied block while holding the Share lock.
# File 'activesupport/lib/active_support/concurrency/share_lock.rb', line 162
def sharing start_sharing begin yield ensure stop_sharing end end
#start_exclusive(purpose: nil, compatible: [], no_wait: false)
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 owner 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 an owner
is awaiting a lock, it is not running any other code. With
purpose matching, it is possible to yield only to other
owners whose activity will not interfere.
# File 'activesupport/lib/active_support/concurrency/share_lock.rb', line 77
def start_exclusive(purpose: nil, compatible: [], no_wait: false) synchronize do unless @exclusive_owner == current_owner if busy_for_exclusive?(purpose) return false if no_wait yield_shares(purpose: purpose, compatible: compatible, block_share: true) do wait_for(:start_exclusive) { busy_for_exclusive?(purpose) } end end @exclusive_owner = current_owner end @exclusive_depth += 1 true end end
#start_sharing
[ GitHub ]# File 'activesupport/lib/active_support/concurrency/share_lock.rb', line 115
def start_sharing synchronize do owner = current_owner if @sharing[owner] > 0 || @exclusive_owner == owner # We already hold a lock; nothing to wait for elsif @waiting[owner] # We're nested inside a yield_shares call: we'll resume as # soon as there isn't an exclusive lock in our way wait_for(:start_sharing) { @exclusive_owner } else # This is an initial / outermost share call: any outstanding # requests for an exclusive lock get to go first wait_for(:start_sharing) { busy_for_sharing?(false) } end @sharing[owner] += 1 end end
#stop_exclusive(compatible: [])
Relinquish the exclusive lock. Must only be called by the owner that called start_exclusive (and currently holds the lock).
# File 'activesupport/lib/active_support/concurrency/share_lock.rb', line 97
def stop_exclusive(compatible: []) synchronize do raise "invalid unlock" if @exclusive_owner != current_owner @exclusive_depth -= 1 if @exclusive_depth == 0 @exclusive_owner = nil if eligible_waiters?(compatible) yield_shares(compatible: compatible, block_share: true) do wait_for(:stop_exclusive) { @exclusive_owner || eligible_waiters?(compatible) } end end @cv.broadcast end end end
#stop_sharing
[ GitHub ]# File 'activesupport/lib/active_support/concurrency/share_lock.rb', line 133
def stop_sharing synchronize do owner = current_owner if @sharing[owner] > 1 @sharing[owner] -= 1 else @sharing.delete owner @cv.broadcast end end end
#wait_for(method, &block) (private)
[ GitHub ]# File 'activesupport/lib/active_support/concurrency/share_lock.rb', line 227
def wait_for(method, &block) owner = current_owner @sleeping[owner] = method @cv.wait_while(&block) ensure @sleeping.delete owner end