abstractx1
abstractx1

Reputation: 459

Ruby please give a simple NON-thread safe example

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

Answers (1)

max pleaner
max pleaner

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

Related Questions