Ruby中的条件变量资源不多,但大多数都是错误的。与ruby-doc,tutorial here或post here一样 - 所有这些都可能陷入僵局。
我们可以通过按给定顺序启动线程来解决问题,并且可能在它们之间放置一些sleep
来强制执行同步。但那只是推迟了真正的问题。
我将代码重写为经典的producer-consumer problem:
require 'thread'
queue = []
mutex = Mutex.new
resource = ConditionVariable.new
threads = []
threads << Thread.new do
5.times do |i|
mutex.synchronize do
resource.wait(mutex)
value = queue.pop
print "consumed #{value}\n"
end
end
end
threads << Thread.new do
5.times do |i|
mutex.synchronize do
queue << i
print "#{i} produced\n"
resource.signal
end
sleep(1) #simulate expense
end
end
threads.each(&:join)
有时你会得到这个(但并非总是如此):
0 produced
1 produced
consumed 0
2 produced
consumed 1
3 produced
consumed 2
4 produced
consumed 3
producer-consumer.rb:30:in `join': deadlock detected (fatal)
from producer-consumer.rb:30:in `each'
from producer-consumer.rb:30:in `<main>'
什么是正确的解决方案?
答案 0 :(得分:2)
问题在于,正如您之前评论的那样,只有在您可以保证消费者线程在程序开始时首先获取互斥锁时,此方法才有效。如果不是这种情况,则会发生死锁,因为生产者线程的第一个resource.signal
将在消费者线程尚未等待资源时发送。因此,第一个resource.signal
基本上不会执行任何操作,因此您最终会调用resource.signal
4次(因为第一个丢失),而resource.wait
被调用5次。这意味着消费者将永远等待,并发生僵局。
幸运的是,我们可以通过仅允许消费者线程开始等待来解决这个问题,如果没有更多的即时工作可用。
require 'thread'
queue = []
mutex = Mutex.new
resource = ConditionVariable.new
threads = []
threads << Thread.new do
5.times do |i|
mutex.synchronize do
if queue.empty?
resource.wait(mutex)
end
value = queue.pop
print "consumed #{value}\n"
end
end
end
threads << Thread.new do
5.times do |i|
mutex.synchronize do
queue << i
print "#{i} produced\n"
resource.signal
end
sleep(1) #simulate expense
end
end
threads.each(&:join)
答案 1 :(得分:1)
这是一个针对多个消费者和生产者的更强大的解决方案,MonitorMixin的使用,MonitorMixin
有一个特殊的ConditionVariable
wait_while()
和wait_until()
方法
require 'monitor'
queue = []
queue.extend(MonitorMixin)
cond = queue.new_cond
consumers, producers = [], []
for i in 0..5
consumers << Thread.start(i) do |i|
print "consumer start #{i}\n"
while (producers.any?(&:alive?) || !queue.empty?)
queue.synchronize do
cond.wait_while { queue.empty? }
print "consumer #{i}: #{queue.shift}\n"
end
sleep(0.2) #simulate expense
end
end
end
for i in 0..3
producers << Thread.start(i) do |i|
id = (65+i).chr
for j in 0..10 do
queue.synchronize do
item = "#{j} #{id}"
queue << item
print "producer #{id}: produced #{item}\n"
j += 1
cond.broadcast
end
sleep(0.1) #simulate expense
end
end
end
sleep 0.1 while producers.any?(&:alive?)
sleep 0.1 while consumers.any?(&:alive?)
print "queue size #{queue.size}\n"
答案 2 :(得分:0)
基于forum thread,我想出了一个有效的解决方案。它强制线程之间的交替,这是不理想的。我们想要消费者和生产者的多个线程是什么?
queue = []
mutex = Mutex.new
threads = []
next_run = :producer
cond_consumer = ConditionVariable.new
cond_producer = ConditionVariable.new
threads << Thread.new do
5.times do |i|
mutex.synchronize do
until next_run == :consumer
cond_consumer.wait(mutex)
end
value = queue.pop
print "consumed #{value}\n"
next_run = :producer
cond_producer.signal
end
end
end
threads << Thread.new do
5.times do |i|
mutex.synchronize do
until next_run == :producer
cond_producer.wait(mutex)
end
queue << i
print "#{i} produced\n"
next_run = :consumer
cond_consumer.signal
end
end
end
threads.each(&:join)
答案 3 :(得分:0)
您可以简化问题:
require 'thread'
queue = Queue.new
consumer = Thread.new { queue.pop }
consumer.join
因为你的主线程正在等待消费者线程退出,但是消费者线程正在休眠(由于queue.pop
),这导致:
producer-consumer.rb:4:in `join': deadlock detected (fatal)
from producer-consumer.rb:4:in `<main>'
所以你必须等待线程完成而不调用join
:
require 'thread'
queue = Queue.new
threads = []
threads << Thread.new do
5.times do |i|
value = queue.pop
puts "consumed #{value}"
end
end
threads << Thread.new do
5.times do |i|
queue << i
puts "#{i} produced"
sleep(1) # simulate expense
end
end
# wait for the threads to finish
sleep(1) while threads.any?(&:alive?)