Your code is buggy: there is a race condition such that the initial #signal
can get called before any corresponding #waits. That it superficially appeared
to work consistently with 1.8 was an accident of thread scheduling. Adding
a 'sleep' beforehand, as another poster suggested, makes it more likely to
work but does not actually fix the bug.
The correct approach is to use a synchronization primitive which queues
notifications, for example a semaphore or a queue, rather than a condition
variable. A queued notification can't get "missed" like this.
-mental
···
On Wed, 20 Feb 2008 22:09:59 +0900, Eric Jacoboni <jaco@neottia.net> wrote:
1.upto(10) do # 10 threads ping
Thread.new do
mutex.synchronize do
pong.wait(mutex)
puts("Ping...")
ping.signal
end
end
end
pong.signal # Go!
Specifically, if "Foo::Semaphore" were a counted semaphore class:
ping = Foo::Semaphore.new(0)
pong = Foo::Semaphore.new(0)
def event(wait, notify, message)
wait.down
puts message
notify.up
end
threads = (1..10).map {
[ Thread.new { event(pong, ping, "Ping...") },
Thread.new { event(ping, pong, "Pong...") } ]
}.flatten
pong.up
threads.each { |t| t.join }
Unfortunately there aren't any good 1.9/JRuby-friendly semaphore
implementations yet. Here is a very simple portable one (public domain):
require 'thread'
class PortableSemaphore
def initialize(count=0)
@lock = Mutex.new
@nonzero = ConditionVariable.new
@count = count
end
def down
@lock.synchronize do
@nonzero.wait @lock until @count.nonzero?
@count -= 1
end
self
end
def up
@lock.synchronize do
@count += 1
@nonzero.signal
end
self
end
end
Note that this is how condition variables are intended to be used --
not directly, but as building blocks for more useful primitives.
-mental
···
On Thu, 21 Feb 2008 03:33:23 +0900, MenTaLguY <mental@rydia.net> wrote:
The correct approach is to use a synchronization primitive which queues
notifications, for example a semaphore or a queue, rather than a condition
variable. A queued notification can't get "missed" like this.
MenTaLguY <mental@rydia.net> writes:
Your code is buggy: there is a race condition such that the initial #signal
can get called before any corresponding #waits.
Gosh... you're right.
Thanks to Guy for the sleep trick and for your PortableSemaphore
implementation: i'm gonna investigate it further.
Please note that you shouldn't ever use the sleep trick in
production code -- it merely hides problems during testing
when they can still occur under production load.
(I emphasize this, because the sleep trick seems to be fairly
popular as an "easy fix"; even I'm guilty of using it a lot
in the past...)
-mental
···
On Thu, 21 Feb 2008 05:59:57 +0900, Eric Jacoboni <jaco@neottia.net> wrote:
Thanks to Guy for the sleep trick and for your PortableSemaphore
implementation: i'm gonna investigate it further.
Mental Guy wrote:
Please note that you shouldn't ever use the sleep trick in
production code -- it merely hides problems during testing
when they can still occur under production load.
Oh yes, i know synchronization should never relies on temporisations...
In this case, it's useful to point the bug you mention in my use of
#signal.
I've had wrote a general semaphore implementation using IO.pipe but your
PortableSemaphore is way more elegant... thanks a lot (i just wonder if
#up/#down honor the FIFO policy)
BTW, as for Queues : i admit i never use them.
···
--
Posted via http://www.ruby-forum.com/\.
I've had wrote a general semaphore implementation using IO.pipe
That can still be useful sometimes -- for example, I wrote a
concurrent-selectable gem which provides latch, semaphore, and
channel (queue) implementations which can be passed as arguments to
IO.select, libev, etc. because they use IO.pipe underneath.
PortableSemaphore is way more elegant... thanks a lot (i just wonder
if #up/#down honor the FIFO policy)
It depends upon the implementation of ConditionVariable#wait. Some
Ruby implementations will wake threads in the order the threads called
#wait, and some will not. Most are roughly FIFO but not 100% "fair".
Fairness actually involves a tradeoff: while unfair blocking
primitives can sometimes lead to starvation (as sufficiently
greedy threads could keep "jumping the queue"), fair primitives
are more likely to have problems with convoying[1].
-mental
[1] Google "lock convoying"
···
On Thu, 21 Feb 2008 06:51:18 +0900, Eric Jacoboni <eric.jacoboni@gmail.com> wrote: