Ruby中的多线程

时间:2018-08-16 23:52:46

标签: ruby multithreading thread-safety

我需要创建3个线程。 每个线程将在屏幕上打印一个颜色,并休眠x秒。 线程A将打印为红色;线程B将显示黄色;线程C将显示为绿色;

所有线程必须等待,直到轮到他们打印为止。 要打印的第一个线程必须是红色,在打印后,红色将告诉黄色该轮到该打印了,依此类推。 线程必须能够多次打印(特定于用户)

我陷入困境是因为在线程外调用@ firstFlag.signal无法正常工作,并且3个线程的工作顺序不正确 我该如何使红色线程优先?

到目前为止,我的代码:

@lock = Mutex.new
@firstFlag = ConditionVariable.new
@secondFlag = ConditionVariable.new
@thirdFlag = ConditionVariable.new

print "Tell me n's vallue:"
@n = gets.to_i

@threads = Array.new

@threads << Thread.new() {
  t = Random.rand(1..3)
  n = 0
  @lock.synchronize {  
    for i in 0...@n do
      @firstFlag.wait(@lock, t)     
      puts "red : #{t}s"
      sleep(t)
      @secondFlag.signal      
    end
  }
}

@threads << Thread.new() {
  t = Random.rand(1..3)
  n = 0
  @lock.synchronize {  
    for i in 0...@n do         
      @secondFlag.wait(@lock, t)     
      puts "yellow : #{t}s"
      sleep(t)
      @thirdFlag.signal
    end
  }
}

@threads << Thread.new() {
  t = Random.rand(1..3)
  n = 0
  @lock.synchronize {  
    for i in 0...@n do      
      @thirdFlag.wait(@lock, t)     
      puts "green : #{t}s"
      sleep(t)
      @firstFlag.signal
    end
  }
}

@threads.each {|t| t.join}
@firstFlag.signal

3 个答案:

答案 0 :(得分:2)

您的代码中存在三个错误:

第一个错误

您的wait呼叫使用超时。这意味着您的线程将与预期的序列不同步,因为超时将使每个线程滑过预期的等待点。

解决方案:将所有等待呼叫更改为不使用超时:

@xxxxFlag.wait(@lock)

第二个错误

您在Thread.join调用结束后放置了序列触发器。您的join调用将永远不会返回,因此代码中的最后一条语句将永远不会执行,线程序列也将不会开始。

解决方案:更改顺序以先发出信号,然后再加入线程:

@firstFlag.signal
@threads.each {|t| t.join} 

第三个错误

wait/signal构造的问题在于它不缓冲信号。 因此,您必须在调用wait之前确保所有线程都处于其signal状态,否则您可能会遇到竞争状态,其中一个线程在另一个线程调用signal之前调用wait。 。

解决方案:尽管可以用Queue解决,但是这很难解决。但是,我建议对您的代码进行彻底的重新思考。完整的解决方案请参见下文。


更好的解决方案

我认为您需要重新考虑整个结构,而不是使用条件变量,而应将Queue用于所有内容。现在,代码变得不那么脆弱了,并且由于Queue本身是线程安全的,因此您不再需要任何关键部分。

Queue的优点是您可以像wait/signal那样使用它,但是它可以缓冲信号,在这种情况下,一切都变得更加简单。

现在我们可以重写代码:

redq    = Queue.new
yellowq = Queue.new
greenq  = Queue.new

然后每个线程变成这样:

@threads << Thread.new() {
  t = Random.rand(1..3)
  n = 0

  for i in 0...@n do
    redq.pop
    puts "red : #{t}s"
    sleep(t)
    yellowq.push(1)
  end
}

最后要开始整个过程​​:

redq.push(1)
@threads.each { |t| t.join }

答案 1 :(得分:1)

我会稍微重新设计一下。将您的ConditionVariable视为标志,线程使用它来表示它已经完成,并相应地命名它们:

@lock = Mutex.new
@thread_a_done = ConditionVariable.new
@thread_b_done = ConditionVariable.new
@thread_c_done = ConditionVariable.new

现在,线程A通过完成@thread_a_done.signal发出信号,线程B可以等待该信号,依此类推。线程A当然需要等待直到线程C完成,因此我们得到了这种结构:

 @threads << Thread.new() {
  t = Random.rand(1..3)
  @lock.synchronize {
    for i in 0...@n do
      @thread_c_done.wait(@lock)
      puts "A: red : #{t}s"
      sleep(t)
      @thread_a_done.signal
    end
  }
}

这里的问题是,您需要确保第一次迭代中的线程A不等待标志信号。毕竟,它是第一位的,因此它不应该等待其他任何人。因此,将其修改为:

@thread_c_done.wait(@lock) unless i == 0 

最后,创建线程后,先调用run,然后在每个线程上join将它们全部踢开(这样程序就不会在最后一个线程完成之前退出):

@threads.each(&:run)
@threads.each(&:join)

哦,顺便说一句,我也会摆脱您的wait中的超时问题。您有一个严格的要求,要求他们按顺序进行。如果使信号等待超时,则可能会导致超时-可以这么说,线程可能仍会“跳过队列”。

编辑,如下所示,@ casper仍然有潜在的竞争条件:线程A可以在线程B等待接收之前调用signal 在这种情况下,线程B会错过它并无限期地等待。解决此问题的一种可能方法是使用某种形式的CountDownLatch-所有线程都可以等待的共享对象,一旦所有线程都表示它们已准备就绪,该对象将被释放。 ruby-concurrency gem具有此实现,并且实际上可能还有其他有趣的东西可用于更优雅的多线程编程。

不过,使用纯红宝石进行标记,您可以通过添加第二个Mutex来解决此问题,该while(in7.hasNextLine()) { for (int sl = 0; sl < list2.length; sl++) { String[] line = in7.nextLine().trim().split(" "); list2[sl] = Double.parseDouble(line); } } 可以保护对布尔标志的共享访问,以指示线程已准备就绪。

答案 2 :(得分:0)

好的,谢谢回答。我找到了解决方法:

我创建了第四个线程。因为我发现在线程外调用“ @ firstFlag.signal”是行不通的,因为ruby有一个“主线程”,当您“运行”其他线程时,它会休眠。 因此,“ @ firstFlag.signal”调用必须在线程内,以便可以在CV.wait的同一级别上。

我使用以下方法解决了这个问题:

@threads << Thread.new {
  sleep 1
  @firstFlag.signal
}

第四个线程将等待1秒钟,然后将第一个信号发送为红色。这仅几秒钟似乎足以使其他线程到达等待点。 而且,正如您的要求一样,我已删除超时。

//编辑//

我意识到我不需要第四个线程,我可以让线程C做第一个信号。 我使线程C睡眠1秒钟,以等待其他两个线程进入等待状态,然后它发出红色信号,也开始等待。

@threads << Thread.new() {
  sleep 1
  @redFlag.signal

  t = Random.rand(1..3)
  n = 0
  @lock.synchronize {  
    for i in 0...@n do      
      @greenFlag.wait(@lock)     
      puts "verde : #{t}s"
      sleep(t)
      @redFlag.signal 
      n += 1
    end
  }
}