diff options
| author | Jean Boussier <jean.boussier@gmail.com> | 2025-12-10 11:44:27 +0100 |
|---|---|---|
| committer | Jean Boussier <jean.boussier@gmail.com> | 2025-12-11 23:25:57 +0100 |
| commit | 07b2356a6ad314b9a7b2bb9fc0527b440f004faa (patch) | |
| tree | 0f11e5431605de6b50b8cd56cb1b4f097a59dd50 /thread_sync.rb | |
| parent | dc58d58a723cf56d2a59db52252b82755248b539 (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.rb | 83 |
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 |
