Feature #19141
openAdd thread-owned Monitor to protect thread-local resources
Description
Background¶
In Ruby v3.0.2, Monitor was modified to be owned by fibers instead of threads for reasons as described in this issue and so it is also consistent with Mutex. Before the change to Monitor, Mutex was modified to per-fiber instead of thread (issue, PR) which caused some problems (See: comment).
Problem¶
We are now encountering a problem where using Enumerator (implemented transparently using Fiber, so the user is not aware) within a Fiber-owned process, which causes a deadlock. That means any framework using Monitor is incompatible to be used with Enumerator.
In general, there are many types of thread-local resources (connections for example), so it would make sense to have a thread-owned monitor to protect them. I think few resources are really fiber-owned.
Specifics¶
- Concurrent Ruby is still designed with per-thread locking, which causes similar incompatibilities. (Read: issue)
- Systems test in Rails implements locking using Monitor, resulting in deadlock in these known cases:
Demo¶
# ruby 2.7.6p219 (2022-04-12 revision c9c2245c0a) [arm64-darwin21]
# Thread #<Thread:0x000000014a8eb228 demo.rb:8 run>, fiber #<Fiber:0x000000014a8eaf80 (resumed)>, locked true, owned true
# Thread #<Thread:0x000000014a8eb228 demo.rb:8 run>, fiber #<Fiber:0x000000014a8eacb0 demo.rb:13 (resumed)>, locked true, owned true
# ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [arm64-darwin21]
# Thread #<Thread:0x0000000102329a08 demo.rb:8 run>, fiber #<Fiber:0x0000000102329828 (resumed)>, locked true, owned true
# Thread #<Thread:0x0000000102329a08 demo.rb:8 run>, fiber #<Fiber:0x00000001023294e0 demo.rb:13 (resumed)>, locked true, owned false
require 'fiber'
require 'monitor'
puts RUBY_DESCRIPTION
# We have a single monitor - we're pretending it protects some thread-local resources
m = Monitor.new
# We'll create an explicit thread
t = Thread.new do
# Lock to protect our thread-local resource
m.enter
puts "Thread #{Thread.current}, fiber #{Fiber.current}, locked #{m.mon_locked?}, owned #{m.mon_owned?}"
# The Enumerator here creates a fiber, which runs on the same thread, so would want to use the same thread-local resource
e = Enumerator.new do |y|
# In 2.7 this is fine, in 3.0 it's not, as the fiber thinks it doesn't have the lock
puts "Thread #{Thread.current}, fiber #{Fiber.current}, locked #{m.mon_locked?}, owned #{m.mon_owned?}"
# This would deadlock
# m.enter
y.yield 1
end
e.next
end
t.join
Possible Solutions¶
- Allow
Monitor
to be per thread or fiber through a flag - Having
Thread::Monitor
andFiber::Monitor
as two separate classes. LeaveMonitor
as it is right now. However, this may not be possible due to theThread::Mutex
alias
These options would give us more flexibility in which type of Monitor to use.