From d8b8a95af9d6aab1e8da2b6f1808bc3ffd406889 Mon Sep 17 00:00:00 2001 From: Jean Boussier Date: Sat, 13 Dec 2025 06:35:58 +0100 Subject: Make Monitor a core class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [Feature #21788] It allows monitor to access internal routines and remove some overhead. Before: ``` ruby 4.0.0dev (2025-12-13T04:52:13Z master 71dd272506) +YJIT +PRISM [arm64-darwin25] Warming up -------------------------------------- Mutex 2.111M i/100ms Monitor 1.736M i/100ms Calculating ------------------------------------- Mutex 25.050M (± 0.4%) i/s (39.92 ns/i) - 126.631M in 5.055208s Monitor 19.809M (± 0.1%) i/s (50.48 ns/i) - 100.672M in 5.082015s ``` After: ``` ruby 4.0.0dev (2025-12-13T06:49:18Z core-monitor 6fabf389fd) +YJIT +PRISM [arm64-darwin25] Warming up -------------------------------------- Mutex 2.144M i/100ms Monitor 1.859M i/100ms Calculating ------------------------------------- Mutex 24.771M (± 0.4%) i/s (40.37 ns/i) - 124.342M in 5.019716s Monitor 23.722M (± 0.4%) i/s (42.15 ns/i) - 118.998M in 5.016361s ``` Bench: ```ruby 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 ``` --- thread_sync.rb | 186 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 182 insertions(+), 4 deletions(-) (limited to 'thread_sync.rb') diff --git a/thread_sync.rb b/thread_sync.rb index 398c0d02b7..c9d37772d7 100644 --- a/thread_sync.rb +++ b/thread_sync.rb @@ -529,9 +529,187 @@ class Thread Primitive.rb_condvar_wait(mutex, timeout) end end + + # Use the Monitor class when you want to have a lock object for blocks with + # mutual exclusion. + # + # lock = Monitor.new + # lock.synchronize do + # # exclusive access + # end + # + # Contrary to Mutex, Monitor is reentrant: + # + # lock = Monitor.new + # lock.synchronize do + # lock.synchronize do + # # exclusive access + # end + # end + class Monitor + # call-seq: + # synchronize { } -> result of the block + # + # Enters exclusive section and executes the block. Leaves the exclusive + # section automatically when the block exits. See example under + # +MonitorMixin+. + def synchronize(&) + Primitive.rb_monitor_synchronize + end + + # call-seq: + # try_enter -> true or false + # + # Attempts to enter exclusive section. Returns +false+ if lock fails. + def try_enter + Primitive.rb_monitor_try_enter + end + + # call-seq: + # enter -> nil + # + # Enters exclusive section. + def enter + Primitive.rb_monitor_enter + end + + # call-seq: + # exit -> nil + # + # Leaves exclusive section. + def exit + Primitive.rb_monitor_exit + end + + # internal methods for MonitorMixin + def mon_check_owner # :nodoc: + Primitive.rb_monitor_check_owner + end + + def mon_locked? # :nodoc: + Primitive.rb_monitor_locked_p + end + + def mon_owned? # :nodoc: + Primitive.rb_monitor_owned_p + end + + # internal methods for MonitorMixin::ConditionVariable + def wait_for_cond(cond, timeout) # :nodoc: + Primitive.rb_monitor_wait_for_cond(cond, timeout) + end + + # Creates a new Monitor::ConditionVariable associated with the + # Monitor object. + # + def new_cond + ConditionVariable.new(self) + end + + # Condition variables, allow to suspend the current thread while in + # the middle of a critical section until a condition is met, such as + # a resource being available. + # + # Example: + # + # monitor = Thread::Monitor.new + # + # resource_available = false + # condvar = monitor.new_cond + # + # a1 = Thread.new { + # # Thread 'a1' waits for the resource to become available and consumes + # # the resource. + # monitor.synchronize { + # condvar.wait_until { resource_available } + # # After the loop, 'resource_available' is guaranteed to be true. + # + # resource_available = false + # puts "a1 consumed the resource" + # } + # } + # + # a2 = Thread.new { + # # Thread 'a2' behaves like 'a1'. + # monitor.synchronize { + # condvar.wait_until { resource_available } + # resource_available = false + # puts "a2 consumed the resource" + # } + # } + # + # b = Thread.new { + # # Thread 'b' periodically makes the resource available. + # loop { + # monitor.synchronize { + # resource_available = true + # + # # Notify one waiting thread if any. It is possible that neither + # # 'a1' nor 'a2 is waiting on 'condvar' at this moment. That's OK. + # condvar.signal + # } + # sleep 1 + # } + # } + # + # # Eventually both 'a1' and 'a2' will have their resources, albeit in an + # # unspecified order. + # [a1, a2].each {|th| th.join} + class ConditionVariable + def initialize(monitor) # :nodoc: + @monitor = monitor + @cond = Thread::ConditionVariable.new + end + + # Releases the lock held in the associated monitor and waits; reacquires the lock on wakeup. + # + # If +timeout+ is given, this method returns after +timeout+ seconds passed, + # even if no other thread doesn't signal. + # + def wait(timeout = nil) + @monitor.mon_check_owner + @monitor.wait_for_cond(@cond, timeout) + end + + # + # Calls wait repeatedly while the given block yields a truthy value. + # + def wait_while + while yield + wait + end + end + + # + # Calls wait repeatedly until the given block yields a truthy value. + # + def wait_until + until yield + wait + end + end + + # + # Wakes up the first thread in line waiting for this lock. + # + def signal + @monitor.mon_check_owner + @cond.signal + end + + # + # Wakes up all threads waiting for this lock. + # + def broadcast + @monitor.mon_check_owner + @cond.broadcast + end + end + end end -Mutex = Thread::Mutex -ConditionVariable = Thread::ConditionVariable -Queue = Thread::Queue -SizedQueue = Thread::SizedQueue +Mutex = Thread::Mutex # :nodoc: +Monitor = Thread::Monitor # :nodoc: +ConditionVariable = Thread::ConditionVariable # :nodoc: +Queue = Thread::Queue # :nodoc: +SizedQueue = Thread::SizedQueue # :nodoc: -- cgit v1.2.3