Project

General

Profile

Actions

Feature #21788

closed

Promote Thread::Monitor to a core class

Feature #21788: Promote Thread::Monitor to a core class

Added by byroot (Jean Boussier) 2 months ago. Updated 11 days ago.

Status:
Closed
Assignee:
-
Target version:
[ruby-core:124278]

Description

Monitor is about as useful as Mutex and yet one is a core class and the other is a "stdlib" extension.

I propose to promote Thread::Monitor as a core class convenience.

The rest of monitor.rb

The monitor stdlib also contains MonitorMixin, but I think this part can remain in lib/monitor.rb (or even be moved to lib/monitor_mixin.rb.

One more questionable part is Monitor#new_cond, as it returns a ::MonitorMixin::ConditionVariable so it would also need to remain in the stdlib.

Performance benefit

By being a core class, Monitor can also be implemented more efficiently, as it is no longer constrained to the public C API.
On my branch, monitor is approximately 15% faster (but there might be some more optimizations to be found).

master:

ruby 4.0.0dev (2025-12-17T07:16:32Z master 54d3945ee5) +YJIT +PRISM [arm64-darwin25]
Warming up --------------------------------------
               Mutex     1.952M i/100ms
             Monitor     1.775M i/100ms
Calculating -------------------------------------
               Mutex     25.102M (± 0.4%) i/s   (39.84 ns/i) -    126.884M in   5.054731s
             Monitor     21.189M (± 0.1%) i/s   (47.19 ns/i) -    106.485M in   5.025493s

core monitor:

ruby 4.0.0dev (2025-12-17T07:21:34Z core-monitor 32815a1bc4) +YJIT +PRISM [arm64-darwin25]
Warming up --------------------------------------
               Mutex     1.975M i/100ms
             Monitor     1.936M i/100ms
Calculating -------------------------------------
               Mutex     25.213M (± 0.4%) i/s   (39.66 ns/i) -    126.407M in   5.013728s
             Monitor     24.099M (± 0.1%) i/s   (41.50 ns/i) -    121.992M in   5.062092s

Proposed implementation: https://github.com/ruby/ruby/pull/15538

Updated by Eregon (Benoit Daloze) 2 months ago · Edited Actions #1 [ruby-core:124285]

+1

I think MonitorMixin and MonitorMixin::ConditionVariable are small enough that it would be good to have them in core too.
BTW wait_while/wait_until are good patterns and maybe something Thread::ConditionVariable should have too, out-of-topic I know but the point is MonitorMixin seems well done and core-worthy.
mon_initialize could also be made safer potentially if it's made core, e.g. to use an internal lock on the object (or the GVL) or so.

Updated by byroot (Jean Boussier) 2 months ago Actions #2

  • Description updated (diff)

Updated by byroot (Jean Boussier) about 2 months ago Actions #3

the point is MonitorMixin seems well done and core-worthy.

Yeah, I don't think I agree with this. I never use it because including it expose a bunch of mon_* public methods that nobody ever use.
At the end of the day 99% of Monitor usage is just:

class MyClass
  def initialize
    @monitor = Monitor.new
  end

  def some_method
    @monitor.synchronize do
      # do thing
    end
  end
end

I really see no value in the mixin.

Updated by shugo (Shugo Maeda) about 1 month ago Actions #4 [ruby-core:124500]

I am neutral on whether to make monitor built-in partially or fully, but backward compatibility for code that calls require 'monitor' should be maintained.

Speaking of MonitorMixin, it's just a shorthand form inspired by Java's synchronized mechanism, where the object representing the protected resource itself acts as a synchronization primitive.
However, MonitorMixin::ConditionVariable is even more critical, as it's essential for coordinating with other threads that need to access the protected resource.
In Java's synchronized mechanism, the protected object itself also serves as a condition variable. However, this necessitates using broadcast (notifyAll) instead of signal (notify) when waiting on multiple conditions. To address this, monitor decouples condition variables into separate objects.

Updated by ko1 (Koichi Sasada) about 1 month ago Actions #5 [ruby-core:124521]

Monitor is about as useful as Mutex and yet one is a core class and the other is a "stdlib" extension.

Do you mean it is useful because of recursive mutex? If it is the reason, Mutex.new(recursive: true) or something similar is another idea.

Updated by byroot (Jean Boussier) about 1 month ago Actions #6 [ruby-core:124526]

Do you mean it is useful because of recursive mutex?

It's very often used as one.

In my case I'm interested in speeding it up, to help speedup synchronized constructs such as connection pools, so I need the condition variable.

Updated by Eregon (Benoit Daloze) about 1 month ago Actions #7 [ruby-core:124549]

ko1 (Koichi Sasada) wrote in #note-5:

Do you mean it is useful because of recursive mutex? If it is the reason, Mutex.new(recursive: true) or something similar is another idea.

Mutex is already very well established as "non-recursive/non-reentrant" and Monitor as recursive/reentrant so I think Mutex.new(recursive: true) would only add confusion now.

I think it's a good idea to make Monitor core, and I mostly agree with the PR at https://github.com/ruby/ruby/pull/15538.
Main remaining discussion point for me is https://github.com/ruby/ruby/pull/15538#discussion_r2659721652, i.e. I think all Monitor methods should be in core, and monitor.rb would only contain MonitorMixin.
MonitorMixin::ConditionVariable is moved to Monitor::ConditionVariable which is a good cleanup as that's usable without MonitorMixin.

Updated by Eregon (Benoit Daloze) about 1 month ago Actions #8 [ruby-core:124550]

BTW both Mutex and Monitor support ConditionVariable, so I don't think there is an argument that a Monitor is more than just a reentrant Mutex, it's not, it's just that.

Updated by ko1 (Koichi Sasada) 14 days ago Actions #9 [ruby-core:124751]

BTW, why performance is improved?

Updated by byroot (Jean Boussier) 14 days ago Actions #10 [ruby-core:124753]

BTW, why performance is improved?

For the most part is because being in core allow to remove many GET_EC() calls.

First being in core we can use the Primitive API, so we recieve the ec as first argument, then being inside thread_sync.c, we can directly pass down the ec to rb_mut_trylock and a few other routines.

GET_EC() is quite costly as it was made non-inlineable: https://github.com/ruby/ruby/blob/2967f98f305b95e7c910facd2d9b3381bd0296a8/vm_core.h#L2116-L2128

Updated by byroot (Jean Boussier) 11 days ago Actions #11

  • Status changed from Open to Closed

Applied in changeset git|d8b8a95af9d6aab1e8da2b6f1808bc3ffd406889.


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:

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
Actions

Also available in: PDF Atom