ruby/test/-ext-/thread/test_instrumentation_api.rb
Koichi Sasada ef2bb61018 Ractor::Port
* Added `Ractor::Port`
  * `Ractor::Port#receive` (support multi-threads)
  * `Rcator::Port#close`
  * `Ractor::Port#closed?`
* Added some methods
  * `Ractor#join`
  * `Ractor#value`
  * `Ractor#monitor`
  * `Ractor#unmonitor`
* Removed some methods
  * `Ractor#take`
  * `Ractor.yield`
* Change the spec
  * `Racotr.select`

You can wait for multiple sequences of messages with `Ractor::Port`.

```ruby
ports = 3.times.map{ Ractor::Port.new }
ports.map.with_index do |port, ri|
  Ractor.new port,ri do |port, ri|
    3.times{|i| port << "r#{ri}-#{i}"}
  end
end

p ports.each{|port| pp 3.times.map{port.receive}}

```

In this example, we use 3 ports, and 3 Ractors send messages to them respectively.
We can receive a series of messages from each port.

You can use `Ractor#value` to get the last value of a Ractor's block:

```ruby
result = Ractor.new do
  heavy_task()
end.value
```

You can wait for the termination of a Ractor with `Ractor#join` like this:

```ruby
Ractor.new do
  some_task()
end.join
```

`#value` and `#join` are similar to `Thread#value` and `Thread#join`.

To implement `#join`, `Ractor#monitor` (and `Ractor#unmonitor`) is introduced.

This commit changes `Ractor.select()` method.
It now only accepts ports or Ractors, and returns when a port receives a message or a Ractor terminates.

We removes `Ractor.yield` and `Ractor#take` because:
* `Ractor::Port` supports most of similar use cases in a simpler manner.
* Removing them significantly simplifies the code.

We also change the internal thread scheduler code (thread_pthread.c):
* During barrier synchronization, we keep the `ractor_sched` lock to avoid deadlocks.
  This lock is released by `rb_ractor_sched_barrier_end()`
  which is called at the end of operations that require the barrier.
* fix potential deadlock issues by checking interrupts just before setting UBF.

https://bugs.ruby-lang.org/issues/21262
2025-05-31 04:01:33 +09:00

292 lines
7.1 KiB
Ruby

# frozen_string_literal: false
require 'envutil'
require_relative "helper"
class TestThreadInstrumentation < Test::Unit::TestCase
include ThreadInstrumentation::TestHelper
def setup
pend("No windows support") if /mswin|mingw|bccwin/ =~ RUBY_PLATFORM
require '-test-/thread/instrumentation'
cleanup_threads
end
def teardown
return if /mswin|mingw|bccwin/ =~ RUBY_PLATFORM
Bug::ThreadInstrumentation.unregister_callback
cleanup_threads
end
THREADS_COUNT = 3
def test_single_thread_timeline
thread = nil
full_timeline = record do
thread = Thread.new { 1 + 1 }
thread.join
end
assert_equal %i(started ready resumed suspended exited), timeline_for(thread, full_timeline)
ensure
thread&.kill
end
def test_thread_pass_single_thread
full_timeline = record do
Thread.pass
end
assert_equal [], timeline_for(Thread.current, full_timeline)
end
def test_thread_pass_multi_thread
thread = Thread.new do
cpu_bound_work(0.5)
end
full_timeline = record do
Thread.pass
end
assert_equal %i(suspended ready resumed), timeline_for(Thread.current, full_timeline)
ensure
thread&.kill
thread&.join
end
def test_multi_thread_timeline
threads = nil
full_timeline = record do
threads = threaded_cpu_bound_work(1.0)
results = threads.map(&:value)
results.each do |r|
refute_equal false, r
end
assert_equal [false] * THREADS_COUNT, threads.map(&:status)
end
threads.each do |thread|
timeline = timeline_for(thread, full_timeline)
assert_consistent_timeline(timeline)
assert_operator timeline.count(:suspended), :>=, 1, "Expected threads to yield suspended at least once: #{timeline.inspect}"
end
timeline = timeline_for(Thread.current, full_timeline)
assert_consistent_timeline(timeline)
ensure
threads&.each(&:kill)
end
def test_join_suspends # Bug #18900
thread = other_thread = nil
full_timeline = record do
other_thread = Thread.new { sleep 0.3 }
thread = Thread.new { other_thread.join }
thread.join
end
timeline = timeline_for(thread, full_timeline)
assert_consistent_timeline(timeline)
assert_equal %i(started ready resumed suspended ready resumed suspended exited), timeline
ensure
other_thread&.kill
thread&.kill
end
def test_io_release_gvl
r, w = IO.pipe
thread = nil
full_timeline = record do
thread = Thread.new do
w.write("Hello\n")
end
thread.join
end
timeline = timeline_for(thread, full_timeline)
assert_consistent_timeline(timeline)
assert_equal %i(started ready resumed suspended ready resumed suspended exited), timeline
ensure
r&.close
w&.close
end
def test_queue_releases_gvl
queue1 = Queue.new
queue2 = Queue.new
thread = nil
full_timeline = record do
thread = Thread.new do
queue1 << true
queue2.pop
end
queue1.pop
queue2 << true
thread.join
end
timeline = timeline_for(thread, full_timeline)
assert_consistent_timeline(timeline)
assert_equal %i(started ready resumed suspended ready resumed suspended exited), timeline
end
def test_blocking_on_ractor
assert_ractor(<<-"RUBY", require_relative: "helper", require: "-test-/thread/instrumentation")
include ThreadInstrumentation::TestHelper
ractor = Ractor.new {
Ractor.receive # wait until woke
Thread.current
}
# Wait for the main thread to block, then wake the ractor
Thread.new do
while Thread.main.status != "sleep"
Thread.pass
end
ractor.send true
end
full_timeline = record do
ractor.value
end
timeline = timeline_for(Thread.current, full_timeline)
assert_consistent_timeline(timeline)
assert_equal %i(suspended ready resumed), timeline
RUBY
end
def test_sleeping_inside_ractor
omit "This test is flaky and intermittently failing now on ModGC workflow" if ENV['GITHUB_WORKFLOW'] == 'ModGC'
assert_ractor(<<-"RUBY", require_relative: "helper", require: "-test-/thread/instrumentation")
include ThreadInstrumentation::TestHelper
thread = nil
full_timeline = record do
thread = Ractor.new{
sleep 0.1
Thread.current
}.value
sleep 0.1
end
timeline = timeline_for(thread, full_timeline)
assert_consistent_timeline(timeline)
assert_equal %i(started ready resumed suspended ready resumed suspended exited), timeline
RUBY
end
def test_thread_blocked_forever_on_mutex
mutex = Mutex.new
mutex.lock
thread = nil
full_timeline = record do
thread = Thread.new do
mutex.lock
end
10.times { Thread.pass }
sleep 0.1
end
mutex.unlock
thread.join
timeline = timeline_for(thread, full_timeline)
assert_consistent_timeline(timeline)
assert_equal %i(started ready resumed suspended), timeline
end
def test_thread_blocked_temporarily_on_mutex
mutex = Mutex.new
mutex.lock
thread = nil
full_timeline = record do
thread = Thread.new do
mutex.lock
end
10.times { Thread.pass }
sleep 0.1
mutex.unlock
10.times { Thread.pass }
sleep 0.1
end
thread.join
timeline = timeline_for(thread, full_timeline)
assert_consistent_timeline(timeline)
assert_equal %i(started ready resumed suspended ready resumed suspended exited), timeline
end
def test_thread_instrumentation_fork_safe
skip "No fork()" unless Process.respond_to?(:fork)
thread_statuses = full_timeline = nil
IO.popen("-") do |read_pipe|
if read_pipe
thread_statuses = Marshal.load(read_pipe)
full_timeline = Marshal.load(read_pipe)
else
threads = threaded_cpu_bound_work.each(&:join)
Marshal.dump(threads.map(&:status), STDOUT)
full_timeline = Bug::ThreadInstrumentation.unregister_callback.map { |t, e| [t.to_s, e ] }
Marshal.dump(full_timeline, STDOUT)
end
end
assert_predicate $?, :success?
assert_equal [false] * THREADS_COUNT, thread_statuses
thread_names = full_timeline.map(&:first).uniq
thread_names.each do |thread_name|
assert_consistent_timeline(timeline_for(thread_name, full_timeline))
end
end
def test_thread_instrumentation_unregister
assert Bug::ThreadInstrumentation::register_and_unregister_callbacks
end
private
def fib(n = 30)
return n if n <= 1
fib(n-1) + fib(n-2)
end
def cpu_bound_work(duration)
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + duration
i = 0
while deadline > Process.clock_gettime(Process::CLOCK_MONOTONIC)
fib(25)
i += 1
end
i > 0 ? i : false
end
def threaded_cpu_bound_work(duration = 0.5)
THREADS_COUNT.times.map do
Thread.new do
cpu_bound_work(duration)
end
end
end
def cleanup_threads
Thread.list.each do |thread|
if thread != Thread.current
thread.kill
thread.join rescue nil
end
end
assert_equal [Thread.current], Thread.list
end
end