mirror of
https://github.com/ruby/ruby.git
synced 2026-01-26 20:19:19 +00:00
That call is surprisingly expensive, so trying doing it once
in `#synchronize` and then passing the EC to lock and unlock
saves quite a few cycles.
Before:
```
ruby 4.0.0dev (2025-12-10T09:30:18Z master c5608ab4d7) +YJIT +PRISM [arm64-darwin25]
Warming up --------------------------------------
Mutex 1.888M i/100ms
Monitor 1.633M i/100ms
Calculating -------------------------------------
Mutex 22.610M (± 0.2%) i/s (44.23 ns/i) - 113.258M in 5.009097s
Monitor 19.148M (± 0.3%) i/s (52.22 ns/i) - 96.366M in 5.032755s
```
After:
```
ruby 4.0.0dev (2025-12-10T10:40:07Z speedup-mutex 1c901cd4f8) +YJIT +PRISM [arm64-darwin25]
Warming up --------------------------------------
Mutex 2.095M i/100ms
Monitor 1.578M i/100ms
Calculating -------------------------------------
Mutex 24.456M (± 0.4%) i/s (40.89 ns/i) - 123.584M in 5.053418s
Monitor 19.176M (± 0.1%) i/s (52.15 ns/i) - 96.243M in 5.018977s
```
Bench:
```
require 'bundler/inline'
gemfile do
gem "benchmark-ips"
end
mutex = Mutex.new
require "monitor"
monitor = Monitor.new
Benchmark.ips do |x|
x.report("Mutex") { mutex.synchronize { } }
x.report("Monitor") { monitor.synchronize { } }
end
```
152 lines
4.5 KiB
Ruby
152 lines
4.5 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class Thread
|
|
class Queue
|
|
# call-seq:
|
|
# pop(non_block=false, timeout: nil)
|
|
#
|
|
# Retrieves data from the queue.
|
|
#
|
|
# If the queue is empty, the calling thread is suspended until data is pushed
|
|
# onto the queue. If +non_block+ is true, the thread isn't suspended, and
|
|
# +ThreadError+ is raised.
|
|
#
|
|
# If +timeout+ seconds have passed and no data is available +nil+ is
|
|
# returned. If +timeout+ is +0+ it returns immediately.
|
|
def pop(non_block = false, timeout: nil)
|
|
if non_block && timeout
|
|
raise ArgumentError, "can't set a timeout if non_block is enabled"
|
|
end
|
|
Primitive.rb_queue_pop(non_block, timeout)
|
|
end
|
|
alias_method :deq, :pop
|
|
alias_method :shift, :pop
|
|
end
|
|
|
|
class SizedQueue
|
|
# call-seq:
|
|
# pop(non_block=false, timeout: nil)
|
|
#
|
|
# Retrieves data from the queue.
|
|
#
|
|
# If the queue is empty, the calling thread is suspended until data is
|
|
# pushed onto the queue. If +non_block+ is true, the thread isn't
|
|
# suspended, and +ThreadError+ is raised.
|
|
#
|
|
# If +timeout+ seconds have passed and no data is available +nil+ is
|
|
# returned. If +timeout+ is +0+ it returns immediately.
|
|
def pop(non_block = false, timeout: nil)
|
|
if non_block && timeout
|
|
raise ArgumentError, "can't set a timeout if non_block is enabled"
|
|
end
|
|
Primitive.rb_szqueue_pop(non_block, timeout)
|
|
end
|
|
alias_method :deq, :pop
|
|
alias_method :shift, :pop
|
|
|
|
# call-seq:
|
|
# push(object, non_block=false, timeout: nil)
|
|
# enq(object, non_block=false, timeout: nil)
|
|
# <<(object)
|
|
#
|
|
# Pushes +object+ to the queue.
|
|
#
|
|
# If there is no space left in the queue, waits until space becomes
|
|
# available, unless +non_block+ is true. If +non_block+ is true, the
|
|
# thread isn't suspended, and +ThreadError+ is raised.
|
|
#
|
|
# If +timeout+ seconds have passed and no space is available +nil+ is
|
|
# returned. If +timeout+ is +0+ it returns immediately.
|
|
# Otherwise it returns +self+.
|
|
def push(object, non_block = false, timeout: nil)
|
|
if non_block && timeout
|
|
raise ArgumentError, "can't set a timeout if non_block is enabled"
|
|
end
|
|
Primitive.rb_szqueue_push(object, non_block, timeout)
|
|
end
|
|
alias_method :enq, :push
|
|
alias_method :<<, :push
|
|
end
|
|
|
|
class Mutex
|
|
# call-seq:
|
|
# Thread::Mutex.new -> mutex
|
|
#
|
|
# Creates a new Mutex
|
|
def initialize
|
|
end
|
|
|
|
# call-seq:
|
|
# mutex.locked? -> true or false
|
|
#
|
|
# Returns +true+ if this lock is currently held by some thread.
|
|
def locked?
|
|
Primitive.cexpr! %q{ RBOOL(mutex_locked_p(mutex_ptr(self))) }
|
|
end
|
|
|
|
# call-seq:
|
|
# mutex.owned? -> true or false
|
|
#
|
|
# Returns +true+ if this lock is currently held by current thread.
|
|
def owned?
|
|
Primitive.rb_mut_owned_p
|
|
end
|
|
|
|
# call-seq:
|
|
# mutex.lock -> self
|
|
#
|
|
# Attempts to grab the lock and waits if it isn't available.
|
|
# Raises +ThreadError+ if +mutex+ was locked by the current thread.
|
|
def lock
|
|
Primitive.rb_mut_lock
|
|
end
|
|
|
|
# call-seq:
|
|
# mutex.try_lock -> true or false
|
|
#
|
|
# Attempts to obtain the lock and returns immediately. Returns +true+ if the
|
|
# lock was granted.
|
|
def try_lock
|
|
Primitive.rb_mut_trylock
|
|
end
|
|
|
|
# call-seq:
|
|
# mutex.lock -> self
|
|
#
|
|
# Attempts to grab the lock and waits if it isn't available.
|
|
# Raises +ThreadError+ if +mutex+ was locked by the current thread.
|
|
def unlock
|
|
Primitive.rb_mut_unlock
|
|
end
|
|
|
|
# call-seq:
|
|
# mutex.synchronize { ... } -> result of the block
|
|
#
|
|
# Obtains a lock, runs the block, and releases the lock when the block
|
|
# completes. See the example under Thread::Mutex.
|
|
def synchronize
|
|
raise ThreadError, "must be called with a block" unless defined?(yield)
|
|
|
|
Primitive.rb_mut_synchronize
|
|
end
|
|
|
|
# call-seq:
|
|
# mutex.sleep(timeout = nil) -> number or nil
|
|
#
|
|
# Releases the lock and sleeps +timeout+ seconds if it is given and
|
|
# non-nil or forever. Raises +ThreadError+ if +mutex+ wasn't locked by
|
|
# the current thread.
|
|
#
|
|
# When the thread is next woken up, it will attempt to reacquire
|
|
# the lock.
|
|
#
|
|
# Note that this method can wakeup without explicit Thread#wakeup call.
|
|
# For example, receiving signal and so on.
|
|
#
|
|
# Returns the slept time in seconds if woken up, or +nil+ if timed out.
|
|
def sleep(timeout = nil)
|
|
Primitive.rb_mut_sleep(timeout)
|
|
end
|
|
end
|
|
end
|