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:
Marc Siegel 2013-12-30 13:20:29 -05:00
parent b0114ee389
commit 49773b66f4
2 changed files with 114 additions and 5 deletions

View File

@ -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

View File

@ -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