Reputation: 459
Can someone please give a concrete example demonstrating non-thread safety? (in a similar manner to a functioning version of mine below if possible)
I need an example class that demonstrates a non-thread safe operation such that I can assert on the failure, and then enforce a Mutex such that I can test that my code is then thread safe.
I have tried the following with no success, as the threads do not appear to run in parallel. Assuming the ruby += operator is not threadsafe, this test always passes when it should not:
class TestLock
attr_reader :sequence
def initialize
@sequence = 0
end
def increment
@sequence += 1
end
end
#RSpec test
it 'does not allow parallel calls to increment' do
test_lock = TestLock.new
threads = []
list1 = []
list2 = []
start_time = Time.now + 2
threads << Thread.new do
loop do
if Time.now > start_time
5000.times { list1 << test_lock.increment }
break
end
end
end
threads << Thread.new do
loop do
if Time.now > start_time
5000.times { list2 << test_lock.increment }
break
end
end
end
threads.each(&:join) # wait for all threads to finish
expect(list1 & list2).to eq([])
end
Upvotes: 1
Views: 269
Reputation: 26788
Here is an example which instead of find a race condition with addition, concatenation, or something like that, uses a blocking file write.
To summarize the parts:
file_write
method performs a blocking write for 2 seconds.file_read
reads the file and assigns it to a global variable to be referenced elsewhere. NonThreadsafe#test
calls these methods in succession, in their own threads, without a mutex. sleep 0.2
is inserted between the calls to ensure that the blocking file write has begun by the time the read is attempted. join
is called on the second thread, so we be sure it's set the read value to a global variable. It returns the read-value from the global variable. Threadsafe#test
does the same thing, but wraps each method call in a mutex.Here it is:
module FileMethods
def file_write(text)
File.open("asd", "w") do |f|
f.write text
sleep 2
end
end
def file_read
$read_val = File.read "asd"
end
end
class NonThreadsafe
include FileMethods
def test
`rm asd`
`touch asd`
Thread.new { file_write("hello") }
sleep 0.2
Thread.new { file_read }.join
$read_val
end
end
class Threadsafe
include FileMethods
def test
`rm asd`
`touch asd`
semaphore = Mutex.new
Thread.new { semaphore.synchronize { file_write "hello" } }
sleep 0.2
Thread.new { semaphore.synchronize { file_read } }.join
$read_val
end
end
And tests:
expect(NonThreadsafe.new.test).to be_empty
expect(Threadsafe.new.test).to eq("hello")
As for an explanation. The reason the non-threadsafe shows the file's read val as empty is because the blocking writing operation is still happening when the read takes place. When you use synchronize the Mutex, though, the write will complete before the read. Note also that the .join
in the threadsafe example takes longer than in the non-threadsafe value - that's because it's sleeping for the full duration specified in the write thread.
Upvotes: 1