Ruby 1.9, threads and FreeBSD 5

Discussion in 'Ruby' started by Eric Jacoboni, Feb 20, 2008.

  1. Hi,

    Considering the following theory code:

    require "thread"

    ping = ConditionVariable.new
    pong = ConditionVariable.new
    mutex = Mutex.new

    1.upto(10) do # 10 threads pong
    Thread.new do
    mutex.synchronize do
    ping.wait(mutex)
    puts("Pong...")
    pong.signal
    end
    end
    end

    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!

    Thread.list.each { |t| t.join if t != Thread.main }


    This code works as expected with Ruby 1.8 on FreeBSD and OS X :

    % /usr/bin/ruby ping_pong_cond.rb
    Ping...Pong...
    Ping...Pong...
    Ping...Pong...
    Ping...Pong...
    Ping...Pong...
    Ping...Pong...
    Ping...Pong...
    Ping...Pong...
    Ping...Pong...
    Ping...Pong...


    But it blocks with Ruby 1.9 on both OS :

    % ruby ping_pong_cond.rb
    ^Cping_pong_cond.rb:30:in `join': Interrupt
    from ping_pong_cond.rb:30:in `block in <main>'
    from ping_pong_cond.rb:30:in `each'
    from ping_pong_cond.rb:30:in `<main>'

    Furthermore, it works fine with Ruby 1.9 on Vista.

    As i know there is some change in Ruby threads/Native threads between
    1.8 and 1.9, i suspect this change could be the culprit...

    Any clue?

    Thanks
    Eric Jacoboni, Feb 20, 2008
    #1
    1. Advertising

  2. Eric Jacoboni

    MenTaLguY Guest

    On Wed, 20 Feb 2008 22:09:59 +0900, Eric Jacoboni <> 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!


    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
    MenTaLguY, Feb 20, 2008
    #2
    1. Advertising

  3. Eric Jacoboni

    MenTaLguY Guest

    On Thu, 21 Feb 2008 03:33:23 +0900, MenTaLguY <> 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.


    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
    MenTaLguY, Feb 20, 2008
    #3
  4. MenTaLguY <> 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.
    Eric Jacoboni, Feb 20, 2008
    #4
  5. Eric Jacoboni

    MenTaLguY Guest

    On Thu, 21 Feb 2008 05:59:57 +0900, Eric Jacoboni <> wrote:
    > 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
    MenTaLguY, Feb 20, 2008
    #5
  6. 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/.
    Eric Jacoboni, Feb 20, 2008
    #6
  7. Eric Jacoboni

    MenTaLguY Guest

    On Thu, 21 Feb 2008 06:51:18 +0900, Eric Jacoboni <> wrote:
    > 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"
    MenTaLguY, Feb 20, 2008
    #7
    1. Advertising

Want to reply to this thread or ask your own question?

It takes just 2 minutes to sign up (and it's free!). Just click the sign up button to choose a username and then you can ask your own questions on the forum.
Similar Threads
  1. snacktime
    Replies:
    4
    Views:
    875
    Andrew MacIntyre
    Feb 8, 2005
  2. Mike C. Fletcher
    Replies:
    0
    Views:
    262
    Mike C. Fletcher
    Apr 18, 2005
  3. Brian Candler
    Replies:
    5
    Views:
    151
  4. Ralph Smith
    Replies:
    6
    Views:
    191
    Pit Capitain
    Oct 26, 2005
  5. Eric Jacoboni

    Ruby 1.9, threads and FreeBSD 5

    Eric Jacoboni, Feb 20, 2008, in forum: Ruby
    Replies:
    3
    Views:
    124
    Eric Jacoboni
    Feb 20, 2008
Loading...

Share This Page