2010-07-28

Queue#pop is unfair in Ruby 1.9

Do you think Queue is fair?

"Fairness" here is, when two or more threads wait with Queue#pop, the control is sequentially passed from the thread for which it waits longest when Queue#push is invoked.

In this sense, Ruby 1.8 is "fair". Following script always dumps "[:go, :first]" for Ruby 1.8.

# http://gist.github.com/493930#file_gistfile1.rb
create_waiter = lambda { |name, command|
t = Thread.new {
p [command.pop, name]
}
Thread.pass until t.status == 'sleep'
t
}

50.times do
command = Queue.new
# create 3 waiters, and...
create_waiter.call(:first, command)
create_waiter.call(:second, command)
create_waiter.call(:third, command)
# and pushes a command.
command.push(:go)
# then try to consume the command by a subsequent thread
Thread.new {
p [command.pop, :LATER]
}
end

In contrast to this, Ruby 1.9 sometimes dumps "[:go, :LATER]" because Queue#pop became unfair from Ruby 1.9.

To tell the truth, there might be no use in making Queue fair because Mutex#lock is unfair in Ruby. However, Queue#pop implementation in Ruby 1.9 is almost fair so if there's a good solution... Anyone?

Anyway, person who is using 1.8 Queue as a thread scheduler should care this change. My typical usage was as follows.

# http://gist.github.com/430066
require 'thread'

q = Queue.new
t1 = Thread.new {
p [:t1, q.pop]
q.push(:t1)
}

q.push(:main)
p [:main, q.pop]

EDITED ADD (8/1): You should wait for the created thread sleeping like the first script on this page.

By the way, Queue#pop in JRuby (both 1.8 and 1.9 modes) is unfair as well but slightly different from Ruby 1.9; only the main thread break in the scheduler. It's written by @mentalguy who is the author or fastthread for Ruby 1.8.

No comments: