Question

I have method a that is invoked repeatedly at some random time, which triggers method b, which is completely executed after some random time and is in it own thread. I want to ensure that a subsequent execution of a waits until b is completed, which is triggered by the current execution of a. In other words, a and b are to be executed alternatively. I tried to do this using mutex and condition variable as follows:

def a
  Thread.new do
    $mutex.synchronize do
      puts "a"
      b
      $cv.wait($mutex)
    end
  end
end

def b
  Thread.new do
    sleep(rand)
    $mutex.synchronize do
      puts "b"
      $cv.signal
    end
  end
end

$mutex, $cv = Mutex.new, ConditionVariable.new
loop{a; sleep(rand)}

In this code, $mutex.synchronize do ... end in method a ensures that $cv.signal (also within another $mutex.synchronize do ... end) in method b is not invoked until $cv.wait($mutex) sets $cv into listening mode for signals. This much is given in the document.

Another function I intended to assign to $mutex.synchronize do ... end in method a is to avoid consecutive execution of method a. My reasoning is that $cv.wait($mutex) in method a should avoid $mutex from being completed and released until $cv.signal in method b is invoked, by which time b should be finished.

I expected that a and b are executed alternatively, thereby printing "a" and "b" alternatively. But in reality, they are not; each of "a" or "b" can be printed consecutively.


After that, I thought that my reasoning above may be wrong in the sense that $mutex is rather completed and released even if $cv (or $mutex) is in waiting mode, once $cv.wait($mutex) has been called. So I added some dummy process to a, changing it to:

def a
  Thread.new do
    $mutex.synchronize do
      puts "a"
      b
      $cv.wait($mutex)
      nil # Dummy process intended to keep `$mutex` locked until `$cv` is released
    end
  end
end

but that did not have effect.


How can this be fixed? Or, what am I wrong about this?

Was it helpful?

Solution 2

I know it sounds strange, but it will be easier to use a queue to block those threads:

def a
  Thread.new do
    $queue.pop
    puts "a"
    b
  end
end

def b
   Thread.new do
    sleep(rand)
    puts "b"
    $queue << true
  end
end

$queue = Queue.new
$queue << true
loop{a; sleep(rand)}

OTHER TIPS

I don't have a solution for you, but isn't the reason a is being called more than you expect is wait releases the lock on the mutex? Otherwise signal could never be called. This seems to happen "as expected" the first time, but after that, you end up having several a threads queued up, itching to enter the synchronize block, and they sneak in before a b thread wakes up and locks the mutex again.

If you poor-man's instrument your code at every turn, you can see it happen:

def a
  puts("a before thread #{Thread.current}")
  Thread.new do
    puts(" a synch0 #{Thread.current}")
    $mutex.synchronize do
      puts("  a before b #{Thread.current}")
      b
      puts("  a after b, before wait #{Thread.current}")
      $cv.wait($mutex)
      puts("  a after wait #{Thread.current}")
    end
    puts(" a synch1 #{Thread.current}")
  end
  puts("a after thread #{Thread.current}")
end

def b
  puts("b before thread #{Thread.current}")
  Thread.new do
    puts(" b before sleep #{Thread.current}")
    sleep(rand)
    puts(" b after sleep, synch0 #{Thread.current}")
    $mutex.synchronize do
      puts("  b before signal #{Thread.current}")
      $cv.signal
      puts("  b after signal #{Thread.current}")
    end
    puts(" b synch1 #{Thread.current}")
  end
  puts("b after thread #{Thread.current}")
end

$mutex, $cv = Mutex.new, ConditionVariable.new
loop{a; sleep(rand)}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top