diff options
| author | Jean Boussier <jean.boussier@gmail.com> | 2025-12-13 06:35:58 +0100 |
|---|---|---|
| committer | Jean Boussier <jean.boussier@gmail.com> | 2026-02-12 20:35:18 +0100 |
| commit | d8b8a95af9d6aab1e8da2b6f1808bc3ffd406889 (patch) | |
| tree | bda49e00ed6d1df7c8dadc6296673500260f65ac | |
| parent | f33073a6cb8e6800309de815a1acce3322401bd1 (diff) | |
Make Monitor a core class
[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
```
| -rw-r--r-- | ext/monitor/depend | 162 | ||||
| -rw-r--r-- | ext/monitor/extconf.rb | 2 | ||||
| -rw-r--r-- | ext/monitor/monitor.c | 301 | ||||
| -rw-r--r-- | lib/monitor.rb (renamed from ext/monitor/lib/monitor.rb) | 79 | ||||
| -rw-r--r-- | spec/ruby/core/kernel/require_spec.rb | 9 | ||||
| -rw-r--r-- | thread_sync.c | 262 | ||||
| -rw-r--r-- | thread_sync.rb | 186 |
7 files changed, 455 insertions, 546 deletions
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/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/ext/monitor/lib/monitor.rb b/lib/monitor.rb index 82d0a75c56..21329a5de7 100644 --- a/ext/monitor/lib/monitor.rb +++ b/lib/monitor.rb @@ -6,9 +6,6 @@ # 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 @@ -89,65 +86,14 @@ require 'monitor.so' # 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. # - 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) @@ -245,26 +191,7 @@ module MonitorMixin 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 +class Monitor # :nodoc: alias try_mon_enter try_enter alias mon_try_enter try_enter alias mon_enter enter 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: |
