mirror of
https://github.com/ruby/ruby.git
synced 2022-11-09 12:17:21 -05:00

thread_sync.c: avoid reaching across stacks of dead threads rb_ensure is insufficient cleanup for fork and we must reinitialize all waitqueues in the child process. Unfortunately this increases the footprint of ConditionVariable, Queue and SizedQueue by 8 bytes on 32-bit (16 bytes on 64-bit). [ruby-core:86316] [Bug #14634] variable.c: fix thread + fork errors in autoload This is fairly non-intrusive bugfix to prevent children from trying to reach into thread stacks of the parent. I will probably reuse this idea and redo r62934, too (same bug). * vm_core.h (typedef struct rb_vm_struct): add fork_gen counter * thread.c (rb_thread_atfork_internal): increment fork_gen * variable.c (struct autoload_data_i): store fork_gen * variable.c (check_autoload_data): remove (replaced with get_...) * variable.c (get_autoload_data): check fork_gen when retrieving * variable.c (check_autoload_required): use get_autoload_data * variable.c (rb_autoloading_value): ditto * variable.c (rb_autoload_p): ditto * variable.c (current_autoload_data): ditto * variable.c (autoload_reset): reset fork_gen, adjust indent * variable.c (rb_autoload_load): set fork_gen when setting state * test/ruby/test_autoload.rb (test_autoload_fork): new test [ruby-core:86410] [Bug #14634] thread_sync: redo r62934 to use fork_gen Instead of maintaining linked-lists to store all rb_queue/rb_szqueue/rb_condvar structs; store only a fork_gen serial number to simplify management of these items. This reduces initialization costs and avoids the up-front cost of resetting all Queue/SizedQueue/ConditionVariable objects at fork while saving 8 bytes per-structure on 64-bit. There are no savings on 32-bit. * thread.c (rb_thread_atfork_internal): remove rb_thread_sync_reset_all call * thread_sync.c (rb_thread_sync_reset_all): remove * thread_sync.c (queue_live): remove * thread_sync.c (queue_free): remove * thread_sync.c (struct rb_queue): s/live/fork_gen/ * thread_sync.c (queue_data_type): use default free * thread_sync.c (queue_alloc): remove list_add * thread_sync.c (queue_fork_check): new function * thread_sync.c (queue_ptr): call queue_fork_check * thread_sync.c (szqueue_free): remove * thread_sync.c (szqueue_data_type): use default free * thread_sync.c (szqueue_alloc): remove list_add * thread_sync.c (szqueue_ptr): check fork_gen via queue_fork_check * thread_sync.c (struct rb_condvar): s/live/fork_gen/ * thread_sync.c (condvar_free): remove * thread_sync.c (cv_data_type): use default free * thread_sync.c (condvar_ptr): check fork_gen * thread_sync.c (condvar_alloc): remove list_add [ruby-core:86316] [Bug #14634] thread_sync.c (condvar_ptr): reset fork_gen after forking Otherwise the condition variable waiter list will always be empty, which is wrong :x [Bug #14725] [Bug #14634] git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/branches/ruby_2_5@66912 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
239 lines
5.2 KiB
Ruby
239 lines
5.2 KiB
Ruby
# frozen_string_literal: false
|
|
require 'test/unit'
|
|
require 'tmpdir'
|
|
|
|
class TestConditionVariable < Test::Unit::TestCase
|
|
ConditionVariable = Thread::ConditionVariable
|
|
Mutex = Thread::Mutex
|
|
|
|
def test_initialized
|
|
assert_raise(TypeError) {
|
|
ConditionVariable.allocate.wait(nil)
|
|
}
|
|
end
|
|
|
|
def test_condvar_signal_and_wait
|
|
mutex = Mutex.new
|
|
condvar = ConditionVariable.new
|
|
result = []
|
|
mutex.synchronize do
|
|
t = Thread.new do
|
|
mutex.synchronize do
|
|
result << 1
|
|
condvar.signal
|
|
end
|
|
end
|
|
|
|
result << 0
|
|
condvar.wait(mutex)
|
|
result << 2
|
|
t.join
|
|
end
|
|
assert_equal([0, 1, 2], result)
|
|
end
|
|
|
|
def test_condvar_wait_exception_handling
|
|
# Calling wait in the only thread running should raise a ThreadError of
|
|
# 'stopping only thread'
|
|
mutex = Mutex.new
|
|
condvar = ConditionVariable.new
|
|
|
|
locked = false
|
|
thread = Thread.new do
|
|
Thread.current.abort_on_exception = false
|
|
mutex.synchronize do
|
|
assert_raise(Interrupt) {
|
|
condvar.wait(mutex)
|
|
}
|
|
locked = mutex.locked?
|
|
end
|
|
end
|
|
|
|
until thread.stop?
|
|
sleep(0.1)
|
|
end
|
|
|
|
thread.raise Interrupt, "interrupt a dead condition variable"
|
|
thread.join
|
|
assert(locked)
|
|
end
|
|
|
|
def test_condvar_wait_and_broadcast
|
|
nr_threads = 3
|
|
threads = Array.new
|
|
mutex = Mutex.new
|
|
condvar = ConditionVariable.new
|
|
result = []
|
|
|
|
nr_threads.times do |i|
|
|
threads[i] = Thread.new do
|
|
mutex.synchronize do
|
|
result << "C1"
|
|
condvar.wait mutex
|
|
result << "C2"
|
|
end
|
|
end
|
|
end
|
|
sleep 0.1
|
|
mutex.synchronize do
|
|
result << "P1"
|
|
condvar.broadcast
|
|
result << "P2"
|
|
end
|
|
Timeout.timeout(5) do
|
|
nr_threads.times do |i|
|
|
threads[i].join
|
|
end
|
|
end
|
|
|
|
assert_equal ["C1", "C1", "C1", "P1", "P2", "C2", "C2", "C2"], result
|
|
end
|
|
|
|
def test_condvar_wait_deadlock
|
|
assert_in_out_err([], <<-INPUT, /\Afatal\nNo live threads left\. Deadlock/, [])
|
|
mutex = Mutex.new
|
|
cv = ConditionVariable.new
|
|
|
|
klass = nil
|
|
mesg = nil
|
|
begin
|
|
mutex.lock
|
|
cv.wait mutex
|
|
mutex.unlock
|
|
rescue Exception => e
|
|
klass = e.class
|
|
mesg = e.message
|
|
end
|
|
puts klass
|
|
print mesg
|
|
INPUT
|
|
end
|
|
|
|
def test_condvar_wait_deadlock_2
|
|
nr_threads = 3
|
|
threads = Array.new
|
|
mutex = Mutex.new
|
|
condvar = ConditionVariable.new
|
|
|
|
nr_threads.times do |i|
|
|
if (i != 0)
|
|
mutex.unlock
|
|
end
|
|
threads[i] = Thread.new do
|
|
mutex.synchronize do
|
|
condvar.wait mutex
|
|
end
|
|
end
|
|
mutex.lock
|
|
end
|
|
|
|
assert_raise(Timeout::Error) do
|
|
Timeout.timeout(0.1) { condvar.wait mutex }
|
|
end
|
|
mutex.unlock
|
|
threads.each(&:kill)
|
|
threads.each(&:join)
|
|
end
|
|
|
|
def test_condvar_timed_wait
|
|
mutex = Mutex.new
|
|
condvar = ConditionVariable.new
|
|
timeout = 0.3
|
|
locked = false
|
|
|
|
t0 = Time.now
|
|
mutex.synchronize do
|
|
begin
|
|
condvar.wait(mutex, timeout)
|
|
ensure
|
|
locked = mutex.locked?
|
|
end
|
|
end
|
|
t1 = Time.now
|
|
t = t1-t0
|
|
|
|
assert_operator(timeout*0.9, :<, t)
|
|
assert(locked)
|
|
end
|
|
|
|
def test_condvar_nolock
|
|
mutex = Mutex.new
|
|
condvar = ConditionVariable.new
|
|
|
|
assert_raise(ThreadError) {condvar.wait(mutex)}
|
|
end
|
|
|
|
def test_condvar_nolock_2
|
|
mutex = Mutex.new
|
|
condvar = ConditionVariable.new
|
|
|
|
Thread.new do
|
|
assert_raise(ThreadError) {condvar.wait(mutex)}
|
|
end.join
|
|
end
|
|
|
|
def test_condvar_nolock_3
|
|
mutex = Mutex.new
|
|
condvar = ConditionVariable.new
|
|
|
|
Thread.new do
|
|
assert_raise(ThreadError) {condvar.wait(mutex, 0.1)}
|
|
end.join
|
|
end
|
|
|
|
def test_condvar_empty_signal
|
|
mutex = Mutex.new
|
|
condvar = ConditionVariable.new
|
|
|
|
assert_nothing_raised(Exception) { mutex.synchronize {condvar.signal} }
|
|
end
|
|
|
|
def test_condvar_empty_broadcast
|
|
mutex = Mutex.new
|
|
condvar = ConditionVariable.new
|
|
|
|
assert_nothing_raised(Exception) { mutex.synchronize {condvar.broadcast} }
|
|
end
|
|
|
|
def test_dup
|
|
bug9440 = '[ruby-core:59961] [Bug #9440]'
|
|
condvar = ConditionVariable.new
|
|
assert_raise(NoMethodError, bug9440) do
|
|
condvar.dup
|
|
end
|
|
end
|
|
|
|
(DumpableCV = ConditionVariable.dup).class_eval {remove_method :marshal_dump}
|
|
|
|
def test_dump
|
|
bug9674 = '[ruby-core:61677] [Bug #9674]'
|
|
condvar = ConditionVariable.new
|
|
assert_raise_with_message(TypeError, /#{ConditionVariable}/, bug9674) do
|
|
Marshal.dump(condvar)
|
|
end
|
|
|
|
condvar = DumpableCV.new
|
|
assert_raise(TypeError, bug9674) do
|
|
Marshal.dump(condvar)
|
|
end
|
|
end
|
|
|
|
def test_condvar_fork
|
|
mutex = Mutex.new
|
|
condvar = ConditionVariable.new
|
|
thrs = (1..10).map do
|
|
Thread.new { mutex.synchronize { condvar.wait(mutex) } }
|
|
end
|
|
thrs.each { 3.times { Thread.pass } }
|
|
pid = fork do
|
|
mutex.synchronize { condvar.broadcast }
|
|
exit!(0)
|
|
end
|
|
_, s = Process.waitpid2(pid)
|
|
assert_predicate s, :success?, 'no segfault [ruby-core:86316] [Bug #14634]'
|
|
until thrs.empty?
|
|
mutex.synchronize { condvar.broadcast }
|
|
thrs.delete_if { |t| t.join(0.01) }
|
|
end
|
|
end if Process.respond_to?(:fork)
|
|
end
|