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 ``` --- ext/monitor/depend | 162 ------------------ ext/monitor/extconf.rb | 2 - ext/monitor/lib/monitor.rb | 289 -------------------------------- ext/monitor/monitor.c | 301 ---------------------------------- lib/monitor.rb | 216 ++++++++++++++++++++++++ spec/ruby/core/kernel/require_spec.rb | 9 +- thread_sync.c | 262 +++++++++++++++++++++++++++++ thread_sync.rb | 186 ++++++++++++++++++++- 8 files changed, 668 insertions(+), 759 deletions(-) delete mode 100644 ext/monitor/depend delete mode 100644 ext/monitor/extconf.rb delete mode 100644 ext/monitor/lib/monitor.rb delete mode 100644 ext/monitor/monitor.c create mode 100644 lib/monitor.rb diff --git a/ext/monitor/depend b/ext/monitor/depend deleted file mode 100644 index 0c7d54afc8..0000000000 --- a/ext/monitor/depend +++ /dev/null @@ -1,162 +0,0 @@ -# AUTOGENERATED DEPENDENCIES START -monitor.o: $(RUBY_EXTCONF_H) -monitor.o: $(arch_hdrdir)/ruby/config.h -monitor.o: $(hdrdir)/ruby/assert.h -monitor.o: $(hdrdir)/ruby/backward.h -monitor.o: $(hdrdir)/ruby/backward/2/assume.h -monitor.o: $(hdrdir)/ruby/backward/2/attributes.h -monitor.o: $(hdrdir)/ruby/backward/2/bool.h -monitor.o: $(hdrdir)/ruby/backward/2/inttypes.h -monitor.o: $(hdrdir)/ruby/backward/2/limits.h -monitor.o: $(hdrdir)/ruby/backward/2/long_long.h -monitor.o: $(hdrdir)/ruby/backward/2/stdalign.h -monitor.o: $(hdrdir)/ruby/backward/2/stdarg.h -monitor.o: $(hdrdir)/ruby/defines.h -monitor.o: $(hdrdir)/ruby/intern.h -monitor.o: $(hdrdir)/ruby/internal/abi.h -monitor.o: $(hdrdir)/ruby/internal/anyargs.h -monitor.o: $(hdrdir)/ruby/internal/arithmetic.h -monitor.o: $(hdrdir)/ruby/internal/arithmetic/char.h -monitor.o: $(hdrdir)/ruby/internal/arithmetic/double.h -monitor.o: $(hdrdir)/ruby/internal/arithmetic/fixnum.h -monitor.o: $(hdrdir)/ruby/internal/arithmetic/gid_t.h -monitor.o: $(hdrdir)/ruby/internal/arithmetic/int.h -monitor.o: $(hdrdir)/ruby/internal/arithmetic/intptr_t.h -monitor.o: $(hdrdir)/ruby/internal/arithmetic/long.h -monitor.o: $(hdrdir)/ruby/internal/arithmetic/long_long.h -monitor.o: $(hdrdir)/ruby/internal/arithmetic/mode_t.h -monitor.o: $(hdrdir)/ruby/internal/arithmetic/off_t.h -monitor.o: $(hdrdir)/ruby/internal/arithmetic/pid_t.h -monitor.o: $(hdrdir)/ruby/internal/arithmetic/short.h -monitor.o: $(hdrdir)/ruby/internal/arithmetic/size_t.h -monitor.o: $(hdrdir)/ruby/internal/arithmetic/st_data_t.h -monitor.o: $(hdrdir)/ruby/internal/arithmetic/uid_t.h -monitor.o: $(hdrdir)/ruby/internal/assume.h -monitor.o: $(hdrdir)/ruby/internal/attr/alloc_size.h -monitor.o: $(hdrdir)/ruby/internal/attr/artificial.h -monitor.o: $(hdrdir)/ruby/internal/attr/cold.h -monitor.o: $(hdrdir)/ruby/internal/attr/const.h -monitor.o: $(hdrdir)/ruby/internal/attr/constexpr.h -monitor.o: $(hdrdir)/ruby/internal/attr/deprecated.h -monitor.o: $(hdrdir)/ruby/internal/attr/diagnose_if.h -monitor.o: $(hdrdir)/ruby/internal/attr/enum_extensibility.h -monitor.o: $(hdrdir)/ruby/internal/attr/error.h -monitor.o: $(hdrdir)/ruby/internal/attr/flag_enum.h -monitor.o: $(hdrdir)/ruby/internal/attr/forceinline.h -monitor.o: $(hdrdir)/ruby/internal/attr/format.h -monitor.o: $(hdrdir)/ruby/internal/attr/maybe_unused.h -monitor.o: $(hdrdir)/ruby/internal/attr/noalias.h -monitor.o: $(hdrdir)/ruby/internal/attr/nodiscard.h -monitor.o: $(hdrdir)/ruby/internal/attr/noexcept.h -monitor.o: $(hdrdir)/ruby/internal/attr/noinline.h -monitor.o: $(hdrdir)/ruby/internal/attr/nonnull.h -monitor.o: $(hdrdir)/ruby/internal/attr/noreturn.h -monitor.o: $(hdrdir)/ruby/internal/attr/packed_struct.h -monitor.o: $(hdrdir)/ruby/internal/attr/pure.h -monitor.o: $(hdrdir)/ruby/internal/attr/restrict.h -monitor.o: $(hdrdir)/ruby/internal/attr/returns_nonnull.h -monitor.o: $(hdrdir)/ruby/internal/attr/warning.h -monitor.o: $(hdrdir)/ruby/internal/attr/weakref.h -monitor.o: $(hdrdir)/ruby/internal/cast.h -monitor.o: $(hdrdir)/ruby/internal/compiler_is.h -monitor.o: $(hdrdir)/ruby/internal/compiler_is/apple.h -monitor.o: $(hdrdir)/ruby/internal/compiler_is/clang.h -monitor.o: $(hdrdir)/ruby/internal/compiler_is/gcc.h -monitor.o: $(hdrdir)/ruby/internal/compiler_is/intel.h -monitor.o: $(hdrdir)/ruby/internal/compiler_is/msvc.h -monitor.o: $(hdrdir)/ruby/internal/compiler_is/sunpro.h -monitor.o: $(hdrdir)/ruby/internal/compiler_since.h -monitor.o: $(hdrdir)/ruby/internal/config.h -monitor.o: $(hdrdir)/ruby/internal/constant_p.h -monitor.o: $(hdrdir)/ruby/internal/core.h -monitor.o: $(hdrdir)/ruby/internal/core/rarray.h -monitor.o: $(hdrdir)/ruby/internal/core/rbasic.h -monitor.o: $(hdrdir)/ruby/internal/core/rbignum.h -monitor.o: $(hdrdir)/ruby/internal/core/rclass.h -monitor.o: $(hdrdir)/ruby/internal/core/rdata.h -monitor.o: $(hdrdir)/ruby/internal/core/rfile.h -monitor.o: $(hdrdir)/ruby/internal/core/rhash.h -monitor.o: $(hdrdir)/ruby/internal/core/robject.h -monitor.o: $(hdrdir)/ruby/internal/core/rregexp.h -monitor.o: $(hdrdir)/ruby/internal/core/rstring.h -monitor.o: $(hdrdir)/ruby/internal/core/rstruct.h -monitor.o: $(hdrdir)/ruby/internal/core/rtypeddata.h -monitor.o: $(hdrdir)/ruby/internal/ctype.h -monitor.o: $(hdrdir)/ruby/internal/dllexport.h -monitor.o: $(hdrdir)/ruby/internal/dosish.h -monitor.o: $(hdrdir)/ruby/internal/error.h -monitor.o: $(hdrdir)/ruby/internal/eval.h -monitor.o: $(hdrdir)/ruby/internal/event.h -monitor.o: $(hdrdir)/ruby/internal/fl_type.h -monitor.o: $(hdrdir)/ruby/internal/gc.h -monitor.o: $(hdrdir)/ruby/internal/glob.h -monitor.o: $(hdrdir)/ruby/internal/globals.h -monitor.o: $(hdrdir)/ruby/internal/has/attribute.h -monitor.o: $(hdrdir)/ruby/internal/has/builtin.h -monitor.o: $(hdrdir)/ruby/internal/has/c_attribute.h -monitor.o: $(hdrdir)/ruby/internal/has/cpp_attribute.h -monitor.o: $(hdrdir)/ruby/internal/has/declspec_attribute.h -monitor.o: $(hdrdir)/ruby/internal/has/extension.h -monitor.o: $(hdrdir)/ruby/internal/has/feature.h -monitor.o: $(hdrdir)/ruby/internal/has/warning.h -monitor.o: $(hdrdir)/ruby/internal/intern/array.h -monitor.o: $(hdrdir)/ruby/internal/intern/bignum.h -monitor.o: $(hdrdir)/ruby/internal/intern/class.h -monitor.o: $(hdrdir)/ruby/internal/intern/compar.h -monitor.o: $(hdrdir)/ruby/internal/intern/complex.h -monitor.o: $(hdrdir)/ruby/internal/intern/cont.h -monitor.o: $(hdrdir)/ruby/internal/intern/dir.h -monitor.o: $(hdrdir)/ruby/internal/intern/enum.h -monitor.o: $(hdrdir)/ruby/internal/intern/enumerator.h -monitor.o: $(hdrdir)/ruby/internal/intern/error.h -monitor.o: $(hdrdir)/ruby/internal/intern/eval.h -monitor.o: $(hdrdir)/ruby/internal/intern/file.h -monitor.o: $(hdrdir)/ruby/internal/intern/hash.h -monitor.o: $(hdrdir)/ruby/internal/intern/io.h -monitor.o: $(hdrdir)/ruby/internal/intern/load.h -monitor.o: $(hdrdir)/ruby/internal/intern/marshal.h -monitor.o: $(hdrdir)/ruby/internal/intern/numeric.h -monitor.o: $(hdrdir)/ruby/internal/intern/object.h -monitor.o: $(hdrdir)/ruby/internal/intern/parse.h -monitor.o: $(hdrdir)/ruby/internal/intern/proc.h -monitor.o: $(hdrdir)/ruby/internal/intern/process.h -monitor.o: $(hdrdir)/ruby/internal/intern/random.h -monitor.o: $(hdrdir)/ruby/internal/intern/range.h -monitor.o: $(hdrdir)/ruby/internal/intern/rational.h -monitor.o: $(hdrdir)/ruby/internal/intern/re.h -monitor.o: $(hdrdir)/ruby/internal/intern/ruby.h -monitor.o: $(hdrdir)/ruby/internal/intern/select.h -monitor.o: $(hdrdir)/ruby/internal/intern/select/largesize.h -monitor.o: $(hdrdir)/ruby/internal/intern/set.h -monitor.o: $(hdrdir)/ruby/internal/intern/signal.h -monitor.o: $(hdrdir)/ruby/internal/intern/sprintf.h -monitor.o: $(hdrdir)/ruby/internal/intern/string.h -monitor.o: $(hdrdir)/ruby/internal/intern/struct.h -monitor.o: $(hdrdir)/ruby/internal/intern/thread.h -monitor.o: $(hdrdir)/ruby/internal/intern/time.h -monitor.o: $(hdrdir)/ruby/internal/intern/variable.h -monitor.o: $(hdrdir)/ruby/internal/intern/vm.h -monitor.o: $(hdrdir)/ruby/internal/interpreter.h -monitor.o: $(hdrdir)/ruby/internal/iterator.h -monitor.o: $(hdrdir)/ruby/internal/memory.h -monitor.o: $(hdrdir)/ruby/internal/method.h -monitor.o: $(hdrdir)/ruby/internal/module.h -monitor.o: $(hdrdir)/ruby/internal/newobj.h -monitor.o: $(hdrdir)/ruby/internal/scan_args.h -monitor.o: $(hdrdir)/ruby/internal/special_consts.h -monitor.o: $(hdrdir)/ruby/internal/static_assert.h -monitor.o: $(hdrdir)/ruby/internal/stdalign.h -monitor.o: $(hdrdir)/ruby/internal/stdbool.h -monitor.o: $(hdrdir)/ruby/internal/stdckdint.h -monitor.o: $(hdrdir)/ruby/internal/symbol.h -monitor.o: $(hdrdir)/ruby/internal/value.h -monitor.o: $(hdrdir)/ruby/internal/value_type.h -monitor.o: $(hdrdir)/ruby/internal/variable.h -monitor.o: $(hdrdir)/ruby/internal/warning_push.h -monitor.o: $(hdrdir)/ruby/internal/xmalloc.h -monitor.o: $(hdrdir)/ruby/missing.h -monitor.o: $(hdrdir)/ruby/ruby.h -monitor.o: $(hdrdir)/ruby/st.h -monitor.o: $(hdrdir)/ruby/subst.h -monitor.o: monitor.c -# AUTOGENERATED DEPENDENCIES END diff --git a/ext/monitor/extconf.rb b/ext/monitor/extconf.rb deleted file mode 100644 index 78c53fa0c5..0000000000 --- a/ext/monitor/extconf.rb +++ /dev/null @@ -1,2 +0,0 @@ -require 'mkmf' -create_makefile('monitor') diff --git a/ext/monitor/lib/monitor.rb b/ext/monitor/lib/monitor.rb deleted file mode 100644 index 82d0a75c56..0000000000 --- a/ext/monitor/lib/monitor.rb +++ /dev/null @@ -1,289 +0,0 @@ -# frozen_string_literal: false -# = monitor.rb -# -# Copyright (C) 2001 Shugo Maeda -# -# This library is distributed under the terms of the Ruby license. -# You can freely distribute/modify this library. -# - -require 'monitor.so' - -# -# In concurrent programming, a monitor is an object or module intended to be -# used safely by more than one thread. The defining characteristic of a -# monitor is that its methods are executed with mutual exclusion. That is, at -# each point in time, at most one thread may be executing any of its methods. -# This mutual exclusion greatly simplifies reasoning about the implementation -# of monitors compared to reasoning about parallel code that updates a data -# structure. -# -# You can read more about the general principles on the Wikipedia page for -# Monitors[https://en.wikipedia.org/wiki/Monitor_%28synchronization%29]. -# -# == Examples -# -# === Simple object.extend -# -# require 'monitor.rb' -# -# buf = [] -# buf.extend(MonitorMixin) -# empty_cond = buf.new_cond -# -# # consumer -# Thread.start do -# loop do -# buf.synchronize do -# empty_cond.wait_while { buf.empty? } -# print buf.shift -# end -# end -# end -# -# # producer -# while line = ARGF.gets -# buf.synchronize do -# buf.push(line) -# empty_cond.signal -# end -# end -# -# The consumer thread waits for the producer thread to push a line to buf -# while buf.empty?. The producer thread (main thread) reads a -# line from ARGF and pushes it into buf then calls empty_cond.signal -# to notify the consumer thread of new data. -# -# === Simple Class include -# -# require 'monitor' -# -# class SynchronizedArray < Array -# -# include MonitorMixin -# -# def initialize(*args) -# super(*args) -# end -# -# alias :old_shift :shift -# alias :old_unshift :unshift -# -# def shift(n=1) -# self.synchronize do -# self.old_shift(n) -# end -# end -# -# def unshift(item) -# self.synchronize do -# self.old_unshift(item) -# end -# end -# -# # other methods ... -# end -# -# +SynchronizedArray+ implements an Array with synchronized access to items. -# This Class is implemented as subclass of Array which includes the -# MonitorMixin module. -# -module MonitorMixin - # - # FIXME: This isn't documented in Nutshell. - # - # Since MonitorMixin.new_cond returns a ConditionVariable, and the example - # above calls while_wait and signal, this class should be documented. - # - class ConditionVariable - # - # 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 - - private - - def initialize(monitor) # :nodoc: - @monitor = monitor - @cond = Thread::ConditionVariable.new - end - end - - def self.extend_object(obj) # :nodoc: - super(obj) - obj.__send__(:mon_initialize) - end - - # - # Attempts to enter exclusive section. Returns +false+ if lock fails. - # - def mon_try_enter - @mon_data.try_enter - end - # For backward compatibility - alias try_mon_enter mon_try_enter - - # - # Enters exclusive section. - # - def mon_enter - @mon_data.enter - end - - # - # Leaves exclusive section. - # - def mon_exit - mon_check_owner - @mon_data.exit - end - - # - # Returns true if this monitor is locked by any thread - # - def mon_locked? - @mon_data.mon_locked? - end - - # - # Returns true if this monitor is locked by current thread. - # - def mon_owned? - @mon_data.mon_owned? - end - - # - # Enters exclusive section and executes the block. Leaves the exclusive - # section automatically when the block exits. See example under - # +MonitorMixin+. - # - def mon_synchronize(&b) - @mon_data.synchronize(&b) - end - alias synchronize mon_synchronize - - # - # Creates a new MonitorMixin::ConditionVariable associated with the - # Monitor object. - # - def new_cond - unless defined?(@mon_data) - mon_initialize - @mon_initialized_by_new_cond = true - end - return ConditionVariable.new(@mon_data) - end - - private - - # Use extend MonitorMixin or include MonitorMixin instead - # of this constructor. Have look at the examples above to understand how to - # use this module. - def initialize(...) - super - mon_initialize - end - - # Initializes the MonitorMixin after being included in a class or when an - # object has been extended with the MonitorMixin - def mon_initialize - if defined?(@mon_data) - if defined?(@mon_initialized_by_new_cond) - return # already initialized. - elsif @mon_data_owner_object_id == self.object_id - raise ThreadError, "already initialized" - end - end - @mon_data = ::Monitor.new - @mon_data_owner_object_id = self.object_id - end - - # Ensures that the MonitorMixin is owned by the current thread, - # otherwise raises an exception. - def mon_check_owner - @mon_data.mon_check_owner - end -end - -# Use the Monitor class when you want to have a lock object for blocks with -# mutual exclusion. -# -# require 'monitor' -# -# lock = Monitor.new -# lock.synchronize do -# # exclusive access -# end -# -class Monitor - # - # Creates a new MonitorMixin::ConditionVariable associated with the - # Monitor object. - # - def new_cond - ::MonitorMixin::ConditionVariable.new(self) - end - - # for compatibility - alias try_mon_enter try_enter - alias mon_try_enter try_enter - alias mon_enter enter - alias mon_exit exit - alias mon_synchronize synchronize -end - -# Documentation comments: -# - All documentation comes from Nutshell. -# - MonitorMixin.new_cond appears in the example, but is not documented in -# Nutshell. -# - All the internals (internal modules Accessible and Initializable, class -# ConditionVariable) appear in RDoc. It might be good to hide them, by -# making them private, or marking them :nodoc:, etc. -# - RDoc doesn't recognise aliases, so we have mon_synchronize documented, but -# not synchronize. -# - mon_owner is in Nutshell, but appears as an accessor in a separate module -# here, so is hard/impossible to RDoc. Some other useful accessors -# (mon_count and some queue stuff) are also in this module, and don't appear -# directly in the RDoc output. -# - in short, it may be worth changing the code layout in this file to make the -# documentation easier diff --git a/ext/monitor/monitor.c b/ext/monitor/monitor.c deleted file mode 100644 index c43751c4e2..0000000000 --- a/ext/monitor/monitor.c +++ /dev/null @@ -1,301 +0,0 @@ -#include "ruby/ruby.h" - -/* Thread::Monitor */ - -struct rb_monitor { - long count; - VALUE owner; - VALUE mutex; -}; - -static void -monitor_mark(void *ptr) -{ - struct rb_monitor *mc = ptr; - rb_gc_mark_movable(mc->owner); - rb_gc_mark_movable(mc->mutex); -} - -static void -monitor_compact(void *ptr) -{ - struct rb_monitor *mc = ptr; - mc->owner = rb_gc_location(mc->owner); - mc->mutex = rb_gc_location(mc->mutex); -} - -static const rb_data_type_t monitor_data_type = { - .wrap_struct_name = "monitor", - .function = { - .dmark = monitor_mark, - .dfree = RUBY_TYPED_DEFAULT_FREE, - .dsize = NULL, // Fully embeded - .dcompact = monitor_compact, - }, - .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_EMBEDDABLE, -}; - -static VALUE -monitor_alloc(VALUE klass) -{ - struct rb_monitor *mc; - VALUE obj; - - obj = TypedData_Make_Struct(klass, struct rb_monitor, &monitor_data_type, mc); - RB_OBJ_WRITE(obj, &mc->mutex, rb_mutex_new()); - RB_OBJ_WRITE(obj, &mc->owner, Qnil); - mc->count = 0; - - return obj; -} - -static struct rb_monitor * -monitor_ptr(VALUE monitor) -{ - struct rb_monitor *mc; - TypedData_Get_Struct(monitor, struct rb_monitor, &monitor_data_type, mc); - return mc; -} - -static bool -mc_owner_p(struct rb_monitor *mc, VALUE current_fiber) -{ - return mc->owner == current_fiber; -} - -/* - * call-seq: - * try_enter -> true or false - * - * Attempts to enter exclusive section. Returns +false+ if lock fails. - */ -static VALUE -monitor_try_enter(VALUE monitor) -{ - struct rb_monitor *mc = monitor_ptr(monitor); - - VALUE current_fiber = rb_fiber_current(); - if (!mc_owner_p(mc, current_fiber)) { - if (!rb_mutex_trylock(mc->mutex)) { - return Qfalse; - } - RB_OBJ_WRITE(monitor, &mc->owner, current_fiber); - mc->count = 0; - } - mc->count += 1; - return Qtrue; -} - - -struct monitor_args { - VALUE monitor; - struct rb_monitor *mc; - VALUE current_fiber; -}; - -static inline void -monitor_args_init(struct monitor_args *args, VALUE monitor) -{ - args->monitor = monitor; - args->mc = monitor_ptr(monitor); - args->current_fiber = rb_fiber_current(); -} - -static void -monitor_enter0(struct monitor_args *args) -{ - if (!mc_owner_p(args->mc, args->current_fiber)) { - rb_mutex_lock(args->mc->mutex); - RB_OBJ_WRITE(args->monitor, &args->mc->owner, args->current_fiber); - args->mc->count = 0; - } - args->mc->count++; -} - -/* - * call-seq: - * enter -> nil - * - * Enters exclusive section. - */ -static VALUE -monitor_enter(VALUE monitor) -{ - struct monitor_args args; - monitor_args_init(&args, monitor); - monitor_enter0(&args); - return Qnil; -} - -static inline void -monitor_check_owner0(struct monitor_args *args) -{ - if (!mc_owner_p(args->mc, args->current_fiber)) { - rb_raise(rb_eThreadError, "current fiber not owner"); - } -} - -/* :nodoc: */ -static VALUE -monitor_check_owner(VALUE monitor) -{ - struct monitor_args args; - monitor_args_init(&args, monitor); - monitor_check_owner0(&args); - return Qnil; -} - -static void -monitor_exit0(struct monitor_args *args) -{ - monitor_check_owner0(args); - - if (args->mc->count <= 0) rb_bug("monitor_exit: count:%d", (int)args->mc->count); - args->mc->count--; - - if (args->mc->count == 0) { - RB_OBJ_WRITE(args->monitor, &args->mc->owner, Qnil); - rb_mutex_unlock(args->mc->mutex); - } -} - -/* - * call-seq: - * exit -> nil - * - * Leaves exclusive section. - */ -static VALUE -monitor_exit(VALUE monitor) -{ - struct monitor_args args; - monitor_args_init(&args, monitor); - monitor_exit0(&args); - return Qnil; -} - -/* :nodoc: */ -static VALUE -monitor_locked_p(VALUE monitor) -{ - struct rb_monitor *mc = monitor_ptr(monitor); - return rb_mutex_locked_p(mc->mutex); -} - -/* :nodoc: */ -static VALUE -monitor_owned_p(VALUE monitor) -{ - struct rb_monitor *mc = monitor_ptr(monitor); - return rb_mutex_locked_p(mc->mutex) && mc_owner_p(mc, rb_fiber_current()) ? Qtrue : Qfalse; -} - -static VALUE -monitor_exit_for_cond(VALUE monitor) -{ - struct rb_monitor *mc = monitor_ptr(monitor); - long cnt = mc->count; - RB_OBJ_WRITE(monitor, &mc->owner, Qnil); - mc->count = 0; - return LONG2NUM(cnt); -} - -struct wait_for_cond_data { - VALUE monitor; - VALUE cond; - VALUE timeout; - VALUE count; -}; - -static VALUE -monitor_wait_for_cond_body(VALUE v) -{ - struct wait_for_cond_data *data = (struct wait_for_cond_data *)v; - struct rb_monitor *mc = monitor_ptr(data->monitor); - // cond.wait(monitor.mutex, timeout) - VALUE signaled = rb_funcall(data->cond, rb_intern("wait"), 2, mc->mutex, data->timeout); - return RTEST(signaled) ? Qtrue : Qfalse; -} - -static VALUE -monitor_enter_for_cond(VALUE v) -{ - // assert(rb_mutex_owned_p(mc->mutex) == Qtrue) - // but rb_mutex_owned_p is not exported... - - struct wait_for_cond_data *data = (struct wait_for_cond_data *)v; - struct rb_monitor *mc = monitor_ptr(data->monitor); - RB_OBJ_WRITE(data->monitor, &mc->owner, rb_fiber_current()); - mc->count = NUM2LONG(data->count); - return Qnil; -} - -/* :nodoc: */ -static VALUE -monitor_wait_for_cond(VALUE monitor, VALUE cond, VALUE timeout) -{ - VALUE count = monitor_exit_for_cond(monitor); - struct wait_for_cond_data data = { - monitor, - cond, - timeout, - count, - }; - - return rb_ensure(monitor_wait_for_cond_body, (VALUE)&data, - monitor_enter_for_cond, (VALUE)&data); -} - -static VALUE -monitor_sync_body(VALUE monitor) -{ - return rb_yield_values(0); -} - -static VALUE -monitor_sync_ensure(VALUE v_args) -{ - monitor_exit0((struct monitor_args *)v_args); - return Qnil; -} - -/* - * 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+. - */ -static VALUE -monitor_synchronize(VALUE monitor) -{ - struct monitor_args args; - monitor_args_init(&args, monitor); - monitor_enter0(&args); - return rb_ensure(monitor_sync_body, (VALUE)&args, monitor_sync_ensure, (VALUE)&args); -} - -void -Init_monitor(void) -{ -#ifdef HAVE_RB_EXT_RACTOR_SAFE - rb_ext_ractor_safe(true); -#endif - - VALUE rb_cMonitor = rb_define_class("Monitor", rb_cObject); - rb_define_alloc_func(rb_cMonitor, monitor_alloc); - - rb_define_method(rb_cMonitor, "try_enter", monitor_try_enter, 0); - rb_define_method(rb_cMonitor, "enter", monitor_enter, 0); - rb_define_method(rb_cMonitor, "exit", monitor_exit, 0); - rb_define_method(rb_cMonitor, "synchronize", monitor_synchronize, 0); - - /* internal methods for MonitorMixin */ - rb_define_method(rb_cMonitor, "mon_locked?", monitor_locked_p, 0); - rb_define_method(rb_cMonitor, "mon_check_owner", monitor_check_owner, 0); - rb_define_method(rb_cMonitor, "mon_owned?", monitor_owned_p, 0); - - /* internal methods for MonitorMixin::ConditionVariable */ - rb_define_method(rb_cMonitor, "wait_for_cond", monitor_wait_for_cond, 2); -} diff --git a/lib/monitor.rb b/lib/monitor.rb new file mode 100644 index 0000000000..21329a5de7 --- /dev/null +++ b/lib/monitor.rb @@ -0,0 +1,216 @@ +# frozen_string_literal: false +# = monitor.rb +# +# Copyright (C) 2001 Shugo Maeda +# +# This library is distributed under the terms of the Ruby license. +# You can freely distribute/modify this library. +# +# +# In concurrent programming, a monitor is an object or module intended to be +# used safely by more than one thread. The defining characteristic of a +# monitor is that its methods are executed with mutual exclusion. That is, at +# each point in time, at most one thread may be executing any of its methods. +# This mutual exclusion greatly simplifies reasoning about the implementation +# of monitors compared to reasoning about parallel code that updates a data +# structure. +# +# You can read more about the general principles on the Wikipedia page for +# Monitors[https://en.wikipedia.org/wiki/Monitor_%28synchronization%29]. +# +# == Examples +# +# === Simple object.extend +# +# require 'monitor.rb' +# +# buf = [] +# buf.extend(MonitorMixin) +# empty_cond = buf.new_cond +# +# # consumer +# Thread.start do +# loop do +# buf.synchronize do +# empty_cond.wait_while { buf.empty? } +# print buf.shift +# end +# end +# end +# +# # producer +# while line = ARGF.gets +# buf.synchronize do +# buf.push(line) +# empty_cond.signal +# end +# end +# +# The consumer thread waits for the producer thread to push a line to buf +# while buf.empty?. The producer thread (main thread) reads a +# line from ARGF and pushes it into buf then calls empty_cond.signal +# to notify the consumer thread of new data. +# +# === Simple Class include +# +# require 'monitor' +# +# class SynchronizedArray < Array +# +# include MonitorMixin +# +# def initialize(*args) +# super(*args) +# end +# +# alias :old_shift :shift +# alias :old_unshift :unshift +# +# def shift(n=1) +# self.synchronize do +# self.old_shift(n) +# end +# end +# +# def unshift(item) +# self.synchronize do +# self.old_unshift(item) +# end +# end +# +# # other methods ... +# end +# +# +SynchronizedArray+ implements an Array with synchronized access to items. +# This Class is implemented as subclass of Array which includes the +# MonitorMixin module. +# +module MonitorMixin + ConditionVariable = Monitor::ConditionVariable # :nodoc: + + # + # FIXME: This isn't documented in Nutshell. + # + # Since MonitorMixin.new_cond returns a ConditionVariable, and the example + # above calls while_wait and signal, this class should be documented. + # + + def self.extend_object(obj) # :nodoc: + super(obj) + obj.__send__(:mon_initialize) + end + + # + # Attempts to enter exclusive section. Returns +false+ if lock fails. + # + def mon_try_enter + @mon_data.try_enter + end + # For backward compatibility + alias try_mon_enter mon_try_enter + + # + # Enters exclusive section. + # + def mon_enter + @mon_data.enter + end + + # + # Leaves exclusive section. + # + def mon_exit + mon_check_owner + @mon_data.exit + end + + # + # Returns true if this monitor is locked by any thread + # + def mon_locked? + @mon_data.mon_locked? + end + + # + # Returns true if this monitor is locked by current thread. + # + def mon_owned? + @mon_data.mon_owned? + end + + # + # Enters exclusive section and executes the block. Leaves the exclusive + # section automatically when the block exits. See example under + # +MonitorMixin+. + # + def mon_synchronize(&b) + @mon_data.synchronize(&b) + end + alias synchronize mon_synchronize + + # + # Creates a new MonitorMixin::ConditionVariable associated with the + # Monitor object. + # + def new_cond + unless defined?(@mon_data) + mon_initialize + @mon_initialized_by_new_cond = true + end + return ConditionVariable.new(@mon_data) + end + + private + + # Use extend MonitorMixin or include MonitorMixin instead + # of this constructor. Have look at the examples above to understand how to + # use this module. + def initialize(...) + super + mon_initialize + end + + # Initializes the MonitorMixin after being included in a class or when an + # object has been extended with the MonitorMixin + def mon_initialize + if defined?(@mon_data) + if defined?(@mon_initialized_by_new_cond) + return # already initialized. + elsif @mon_data_owner_object_id == self.object_id + raise ThreadError, "already initialized" + end + end + @mon_data = ::Monitor.new + @mon_data_owner_object_id = self.object_id + end + + # Ensures that the MonitorMixin is owned by the current thread, + # otherwise raises an exception. + def mon_check_owner + @mon_data.mon_check_owner + end +end + +class Monitor # :nodoc: + alias try_mon_enter try_enter + alias mon_try_enter try_enter + alias mon_enter enter + alias mon_exit exit + alias mon_synchronize synchronize +end + +# Documentation comments: +# - All documentation comes from Nutshell. +# - MonitorMixin.new_cond appears in the example, but is not documented in +# Nutshell. +# - All the internals (internal modules Accessible and Initializable, class +# ConditionVariable) appear in RDoc. It might be good to hide them, by +# making them private, or marking them :nodoc:, etc. +# - RDoc doesn't recognise aliases, so we have mon_synchronize documented, but +# not synchronize. +# - mon_owner is in Nutshell, but appears as an accessor in a separate module +# here, so is hard/impossible to RDoc. Some other useful accessors +# (mon_count and some queue stuff) are also in this module, and don't appear +# directly in the RDoc output. +# - in short, it may be worth changing the code layout in this file to make the +# documentation easier diff --git a/spec/ruby/core/kernel/require_spec.rb b/spec/ruby/core/kernel/require_spec.rb index 60d17242fe..da2f48fb61 100644 --- a/spec/ruby/core/kernel/require_spec.rb +++ b/spec/ruby/core/kernel/require_spec.rb @@ -21,13 +21,16 @@ describe "Kernel#require" do provided << "set" provided << "pathname" end + ruby_version_is "4.1" do + provided << "monitor" + end it "#{provided.join(', ')} are already required" do out = ruby_exe("puts $LOADED_FEATURES", options: '--disable-gems --disable-did-you-mean') features = out.lines.map { |line| File.basename(line.chomp, '.*') } # Ignore CRuby internals - features -= %w[encdb transdb windows_1252 windows_31j] + features -= %w[encdb transdb windows_1252 windows_31] features.reject! { |feature| feature.end_with?('-fake') } features.sort.should == provided.sort @@ -37,6 +40,10 @@ describe "Kernel#require" do requires = requires.map { |f| f == "pathname" ? "pathname.so" : f } end + ruby_version_is "4.1" do + requires = requires.map { |f| f == "monitor" ? "monitor.so" : f } + end + code = requires.map { |f| "puts require #{f.inspect}\n" }.join required = ruby_exe(code, options: '--disable-gems') required.should == "false\n" * requires.size diff --git a/thread_sync.c b/thread_sync.c index fa6a60ab62..cf4e3843ff 100644 --- a/thread_sync.c +++ b/thread_sync.c @@ -1261,6 +1261,263 @@ rb_condvar_broadcast(rb_execution_context_t *ec, VALUE self) return self; } +/* Thread::Monitor */ + +struct rb_monitor { + long count; + rb_serial_t ec_serial; + VALUE mutex; +}; + +static void +monitor_mark(void *ptr) +{ + struct rb_monitor *mc = ptr; + rb_gc_mark_movable(mc->mutex); +} + +static void +monitor_compact(void *ptr) +{ + struct rb_monitor *mc = ptr; + mc->mutex = rb_gc_location(mc->mutex); +} + +static const rb_data_type_t monitor_data_type = { + .wrap_struct_name = "monitor", + .function = { + .dmark = monitor_mark, + .dfree = RUBY_TYPED_DEFAULT_FREE, + .dsize = NULL, // Fully embeded + .dcompact = monitor_compact, + }, + .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_EMBEDDABLE, +}; + +static VALUE +monitor_alloc(VALUE klass) +{ + struct rb_monitor *mc; + VALUE obj; + + obj = TypedData_Make_Struct(klass, struct rb_monitor, &monitor_data_type, mc); + RB_OBJ_WRITE(obj, &mc->mutex, rb_mutex_new()); + mc->ec_serial = 0; + mc->count = 0; + + return obj; +} + +static struct rb_monitor * +monitor_ptr(VALUE monitor) +{ + struct rb_monitor *mc; + TypedData_Get_Struct(monitor, struct rb_monitor, &monitor_data_type, mc); + return mc; +} + +static bool +mc_owner_p(struct rb_monitor *mc, rb_serial_t current_fiber_serial) +{ + return mc->ec_serial == current_fiber_serial; +} + +static VALUE +rb_monitor_try_enter(rb_execution_context_t *ec, VALUE monitor) +{ + struct rb_monitor *mc = monitor_ptr(monitor); + + rb_serial_t current_fiber_serial = rb_ec_serial(ec); + if (!mc_owner_p(mc, current_fiber_serial)) { + if (!rb_mut_trylock(ec, mc->mutex)) { + return Qfalse; + } + mc->ec_serial = current_fiber_serial; + mc->count = 0; + } + mc->count += 1; + return Qtrue; +} + +struct monitor_args { + VALUE monitor; + struct rb_monitor *mc; + rb_serial_t current_fiber_serial; + rb_execution_context_t *ec; +}; + +static void +monitor_enter0(struct monitor_args *args) +{ + if (!mc_owner_p(args->mc, args->current_fiber_serial)) { + struct mutex_args mut_args = { + .self = args->mc->mutex, + .mutex = mutex_ptr(args->mc->mutex), + .ec= args->ec, + }; + do_mutex_lock(&mut_args, 1); + args->mc->ec_serial = args->current_fiber_serial; + args->mc->count = 0; + } + args->mc->count++; +} + +static VALUE +rb_monitor_enter(rb_execution_context_t *ec, VALUE monitor) +{ + struct monitor_args args = { + .monitor = monitor, + .mc = monitor_ptr(monitor), + .ec = ec, + .current_fiber_serial = rb_ec_serial(ec), + }; + monitor_enter0(&args); + return Qnil; +} + +static inline void +monitor_check_owner0(struct monitor_args *args) +{ + if (!mc_owner_p(args->mc, args->current_fiber_serial)) { + rb_raise(rb_eThreadError, "current fiber not owner"); + } +} + +static VALUE +rb_monitor_check_owner(rb_execution_context_t *ec, VALUE monitor) +{ + struct monitor_args args = { + .monitor = monitor, + .mc = monitor_ptr(monitor), + .ec = ec, + .current_fiber_serial = rb_ec_serial(ec), + }; + monitor_check_owner0(&args); + return Qnil; +} + +static void +monitor_exit0(struct monitor_args *args) +{ + monitor_check_owner0(args); + + if (args->mc->count <= 0) rb_bug("monitor_exit: count:%d", (int)args->mc->count); + args->mc->count--; + + if (args->mc->count == 0) { + args->mc->ec_serial = 0; + + struct mutex_args mut_args = { + .self = args->mc->mutex, + .mutex = mutex_ptr(args->mc->mutex), + .ec= args->ec, + }; + do_mutex_unlock(&mut_args); + } +} + +static VALUE +rb_monitor_exit(rb_execution_context_t *ec, VALUE monitor) +{ + struct monitor_args args = { + .monitor = monitor, + .mc = monitor_ptr(monitor), + .ec = ec, + .current_fiber_serial = rb_ec_serial(ec), + }; + monitor_exit0(&args); + return Qnil; +} + +static VALUE +rb_monitor_locked_p(rb_execution_context_t *ec, VALUE monitor) +{ + struct rb_monitor *mc = monitor_ptr(monitor); + return rb_mutex_locked_p(mc->mutex); +} + +static VALUE +rb_monitor_owned_p(rb_execution_context_t *ec, VALUE monitor) +{ + struct rb_monitor *mc = monitor_ptr(monitor); + return RBOOL(rb_mutex_locked_p(mc->mutex) && mc_owner_p(mc, rb_ec_serial(ec))); +} + +static VALUE +monitor_exit_for_cond(VALUE monitor) +{ + struct rb_monitor *mc = monitor_ptr(monitor); + long cnt = mc->count; + mc->ec_serial = 0; + mc->count = 0; + return LONG2NUM(cnt); +} + +struct wait_for_cond_data { + VALUE monitor; + VALUE cond; + VALUE timeout; + VALUE count; +}; + +static VALUE +monitor_wait_for_cond_body(VALUE v) +{ + struct wait_for_cond_data *data = (struct wait_for_cond_data *)v; + struct rb_monitor *mc = monitor_ptr(data->monitor); + // cond.wait(monitor.mutex, timeout) + VALUE signaled = rb_funcall(data->cond, rb_intern("wait"), 2, mc->mutex, data->timeout); + return RTEST(signaled) ? Qtrue : Qfalse; +} + +static VALUE +monitor_enter_for_cond(VALUE v) +{ + // assert(rb_mutex_owned_p(mc->mutex) == Qtrue) + // but rb_mutex_owned_p is not exported... + + struct wait_for_cond_data *data = (struct wait_for_cond_data *)v; + struct rb_monitor *mc = monitor_ptr(data->monitor); + mc->ec_serial = rb_ec_serial(GET_EC()); + mc->count = NUM2LONG(data->count); + return Qnil; +} + +static VALUE +rb_monitor_wait_for_cond(rb_execution_context_t *ec, VALUE monitor, VALUE cond, VALUE timeout) +{ + VALUE count = monitor_exit_for_cond(monitor); + struct wait_for_cond_data data = { + monitor, + cond, + timeout, + count, + }; + + return rb_ensure(monitor_wait_for_cond_body, (VALUE)&data, + monitor_enter_for_cond, (VALUE)&data); +} + +static VALUE +monitor_sync_ensure(VALUE v_args) +{ + monitor_exit0((struct monitor_args *)v_args); + return Qnil; +} + +static VALUE +rb_monitor_synchronize(rb_execution_context_t *ec, VALUE monitor) +{ + struct monitor_args args = { + .monitor = monitor, + .mc = monitor_ptr(monitor), + .ec = ec, + .current_fiber_serial = rb_ec_serial(ec), + }; + monitor_enter0(&args); + return rb_ec_ensure(ec, do_ec_yield, (VALUE)ec, monitor_sync_ensure, (VALUE)&args); +} + static void Init_thread_sync(void) { @@ -1283,6 +1540,11 @@ Init_thread_sync(void) id_sleep = rb_intern("sleep"); + /* Monitor */ + VALUE rb_cMonitor = rb_define_class_id_under_no_pin(rb_cThread, rb_intern("Monitor"), rb_cObject); + rb_define_alloc_func(rb_cMonitor, monitor_alloc); + + rb_provide("monitor.so"); rb_provide("thread.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