summaryrefslogtreecommitdiff
path: root/thread_sync.rb
diff options
context:
space:
mode:
authorJean Boussier <jean.boussier@gmail.com>2025-12-10 11:44:27 +0100
committerJean Boussier <jean.boussier@gmail.com>2025-12-11 23:25:57 +0100
commit07b2356a6ad314b9a7b2bb9fc0527b440f004faa (patch)
tree0f11e5431605de6b50b8cd56cb1b4f097a59dd50 /thread_sync.rb
parentdc58d58a723cf56d2a59db52252b82755248b539 (diff)
Mutex: avoid repeated calls to `GET_EC`
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 ```
Diffstat (limited to 'thread_sync.rb')
-rw-r--r--thread_sync.rb83
1 files changed, 83 insertions, 0 deletions
diff --git a/thread_sync.rb b/thread_sync.rb
index f8fa69900b..28c70b1e9c 100644
--- a/thread_sync.rb
+++ b/thread_sync.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Thread
class Queue
# call-seq:
@@ -65,4 +67,85 @@ class Thread
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