Allan
Allan

Reputation: 4710

Ruby threads and mutex

Why does the following ruby code not work?

 2 | require 'thread'
 3 | 
 4 | $mutex = Mutex.new
 5 | $mutex.lock
 6 |
 7 | t = Thread.new {
 8 |   sleep 10
 9 |   $mutex.unlock
10 | }
11 | 
12 | $mutex.lock
13 | puts "Delayed hello"

When I'm running it, I get an error:

./test.rb:13:in `lock': thread 0x7f4557856378 tried to join itself (ThreadError)
    from ./test.rb:13

What is the right way to synchronize two threads without joining them (both threads must continue running after synchronization)?

Upvotes: 3

Views: 6570

Answers (2)

Andrew Hodgkinson
Andrew Hodgkinson

Reputation: 4779

This is old but I'm contributing since it's a bit scary that none of the other answers (at time of writing) seem to be correct. The original code is clearly attempting to:

  • Create a mutex in the main thread and lock it.
  • Start a new thread, which may begin running at any time and after any delay subject to the whims of the Ruby runtime.
  • Have this thread unlock the mutex only once it's finished doing its work.
  • Have the main thread then deliberately re-lock the mutex, with the intention that it's spawned a thread which will unlock it. The main thread waits for that.
  • Then the main thread continues running.

@user2413915: Your solution omits the step of locking again in the main thread, so it won't wait for the spawned thread as intended.

@Paul Rubel: Your code assumes that the spawned thread gets as far as its lock of the mutex before the main thread does. This is a race condition. If the main thread continues to execute and locks first, the spawned thread will be blocked until after the main thread has printed "Delayed hello", which is the exact opposite of the desired outcome. You probably ran it by pasting into the IRB prompt; if you try with your example modified so that the end and Mutex lock are on the same line, it'll fail, printing the message too early (i.e. "end; $mutex.lock"). Either way, it's relying on behaviour of the Ruby runtime that's working by chance.

The original code should actually work fine in principle, albeit arguably lacking in elegance - in practice the Ruby 1.9+ runtime won't allow it as it "sees" two consecutive locks in the main thread without an unlock and doesn't "realise" that there's a spawned thread which is going to do the unlocking. Ruby (in this case technically erroneously) raises a ThreadError deadlock exception.

Instead, make cunning use of the ruby Queue. When you try to pull something off a Queue, the call will block until an item is available. So:

require 'thread'
require 'queue'

queue = Queue.new

t = Thread.new {
  sleep 10
  queue.push( nil ) # Push any object you like - here, it's a NilClass instance
}

queue.pop() # Blocks until thread 't' pushes onto the queue
puts "Delayed hello"

If the spawned thread runs first and pushes onto the queue, then the main thread will just pop the item and keep going. If the main thread tries to pop before the spawned thread pushes, it'll wait for the spawned thread.

[Edit: Note that the object pushed onto the queue could be the results of the spawned thread's processing task, so the main thread gets to wait until processing is complete and get the processing result in one go].

I've tested this on Ruby 1.8.7-p375 and Ruby 2.1.2 via rbenv with success, so it's reasonable to assume that the standard library Queue class is functional across all common major Ruby versions.

Upvotes: 10

fvarj
fvarj

Reputation: 215

You do not need to call the mutex on line 12 again.

require 'thread'

$mutex = Mutex.new
$mutex.lock
t = Thread.new {
  sleep 10
  $mutex.unlock
}

puts "Delayed hello"

This will work.

Upvotes: -2

Related Questions