Add Semaphore#to_mtx returning an unchecked mutex-like facade
Because there is a very significant performance hit for using the mutex implementation, which correctly checks ownership, this implementation formalizes treating a raw semaphore as a faster, unchecked implementation of mutex.
This commit is contained in:
parent
b0114ee389
commit
49773b66f4
|
@ -1,3 +1,5 @@
|
||||||
|
require 'forwardable'
|
||||||
|
|
||||||
module ProcessShared
|
module ProcessShared
|
||||||
module SynchronizableSemaphore
|
module SynchronizableSemaphore
|
||||||
# Yield the block after decrementing the semaphore, ensuring that
|
# Yield the block after decrementing the semaphore, ensuring that
|
||||||
|
@ -12,5 +14,74 @@ module ProcessShared
|
||||||
post
|
post
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Expose an unchecked mutex-like interface using only this semaphore.
|
||||||
|
#
|
||||||
|
# @return [FasterUncheckedMutex] An unchecked mutex facade for this semaphore
|
||||||
|
def to_mtx
|
||||||
|
FasterUncheckedMutex.new(self)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# @api private
|
||||||
|
#
|
||||||
|
# Presents a mutex-like facade over a semaphore.
|
||||||
|
# @see SynchronizableSemaphore#to_mtx
|
||||||
|
#
|
||||||
|
# NOTE: Unlocking a locked mutex from a different process or thread than
|
||||||
|
# that which locked it will result in undefined behavior, whereas with the
|
||||||
|
# Mutex class, this error will be detected and an exception raised.
|
||||||
|
#
|
||||||
|
# It is recommended to develop using the Mutex class, which is checked, and
|
||||||
|
# to use this unchecked variant only to optimized performance for code paths
|
||||||
|
# that have been determined to have correct lock/unlock behavior.
|
||||||
|
class FasterUncheckedMutex
|
||||||
|
extend Forwardable
|
||||||
|
|
||||||
|
def initialize(sem)
|
||||||
|
@sem = sem
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [Boolean] +true+ if currently locked
|
||||||
|
def locked?
|
||||||
|
@sem.try_wait
|
||||||
|
@sem.post
|
||||||
|
false
|
||||||
|
rescue Errno::EAGAIN
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
# Releases the lock and sleeps timeout seconds if it is given and
|
||||||
|
# non-nil or forever.
|
||||||
|
#
|
||||||
|
# TODO: de-duplicate this from Mutex#sleep
|
||||||
|
#
|
||||||
|
# @return [Numeric]
|
||||||
|
def sleep(timeout = nil)
|
||||||
|
unlock
|
||||||
|
begin
|
||||||
|
timeout ? Kernel.sleep(timeout) : Kernel.sleep
|
||||||
|
ensure
|
||||||
|
lock
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [Boolean] +true+ if lock was acquired, +false+ if already locked
|
||||||
|
def try_lock
|
||||||
|
@sem.try_wait
|
||||||
|
true
|
||||||
|
rescue Errno::EAGAIN
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
# delegate to methods with different names
|
||||||
|
def_delegator :@sem, :wait, :lock
|
||||||
|
def_delegator :@sem, :post, :unlock
|
||||||
|
|
||||||
|
# delegate to methods with the same names
|
||||||
|
def_delegators :@sem, :synchronize, :close
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,11 +11,7 @@ module ProcessShared
|
||||||
include LockBehavior
|
include LockBehavior
|
||||||
|
|
||||||
before :each do
|
before :each do
|
||||||
@lock = Semaphore.new
|
@lock = Semaphore.new.to_mtx
|
||||||
class << @lock
|
|
||||||
alias_method :lock, :wait
|
|
||||||
alias_method :unlock, :post
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
after :each do
|
after :each do
|
||||||
|
@ -191,5 +187,47 @@ module ProcessShared
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#to_mtx' do
|
||||||
|
before :each do
|
||||||
|
@mtx = Semaphore.new.to_mtx
|
||||||
|
end
|
||||||
|
|
||||||
|
# NOTE:
|
||||||
|
# - #lock / #unlock covered by LockingBehavior above
|
||||||
|
# - #synchronize covered elsewhere as well?
|
||||||
|
|
||||||
|
describe '#locked?' do
|
||||||
|
it 'returns true when locked' do
|
||||||
|
@mtx.synchronize { @mtx.locked?.must_equal true }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false when not locked' do
|
||||||
|
@mtx.locked?.must_equal false
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not itself acquire lock' do
|
||||||
|
@mtx.locked?.must_equal false
|
||||||
|
@mtx.locked?.must_equal false # check again to make sure lock not acquired
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#sleep' do
|
||||||
|
# TODO: add tests for #sleep
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#try_lock' do
|
||||||
|
it 'returns true and acquires lock when unlocked' do
|
||||||
|
@mtx.try_lock.must_equal true
|
||||||
|
@mtx.locked?.must_equal true
|
||||||
|
@mtx.unlock
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns false when already locked' do
|
||||||
|
@mtx.synchronize { @mtx.try_lock.must_equal false }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue