diff --git a/lib/process_shared/synchronizable_semaphore.rb b/lib/process_shared/synchronizable_semaphore.rb index 38529c8..f381b37 100644 --- a/lib/process_shared/synchronizable_semaphore.rb +++ b/lib/process_shared/synchronizable_semaphore.rb @@ -1,3 +1,5 @@ +require 'forwardable' + module ProcessShared module SynchronizableSemaphore # Yield the block after decrementing the semaphore, ensuring that @@ -12,5 +14,74 @@ module ProcessShared post 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 diff --git a/spec/process_shared/semaphore_spec.rb b/spec/process_shared/semaphore_spec.rb index a0a844c..8bd9db4 100644 --- a/spec/process_shared/semaphore_spec.rb +++ b/spec/process_shared/semaphore_spec.rb @@ -11,11 +11,7 @@ module ProcessShared include LockBehavior before :each do - @lock = Semaphore.new - class << @lock - alias_method :lock, :wait - alias_method :unlock, :post - end + @lock = Semaphore.new.to_mtx end after :each do @@ -191,5 +187,47 @@ module ProcessShared 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