Ruby 如果线程生成速度过快,工作分配将失败

Ruby work distribution fails if threads are generated to fast

前几天我 运行 遇到了一个问题,我花了 2 个小时在错误的地方寻找答案。

在此过程中,我将代码精简为以下版本。只要我在创建线程的循环中有 sleep(0.1),这里的线程就可以工作。

如果省略该行,则会创建所有线程 - 但只有线程 7 会实际使用队列中的数据。

有了这个“hack”,我确实有了一个可行的解决方案,但不是我满意的解决方案。我真的很好奇为什么会这样。

我在 windows 2.4.1p111 下使用相当旧的 ruby 版本。但是,我能够使用新的 ruby 3.0.2p107 安装

重现相同的行为
#!/usr/bin/env ruby

@q = Queue.new
      
# Get all projects (would be a list of directories)
projects = [*0..100]
projects.each do |project|
  @q.push project
end

def worker(num)
  while not @q.empty?
    puts "Thread: #{num} Project: #{@q.pop}"
    sleep(0.5)
  end
end 


threads=[]
for i in 1..7 do
  threads << Thread.new { worker(i) }
  sleep(0.1) # Threading does not work without this line - but why?
end

threads.each {|thread| puts thread.join }

puts "done"

有趣的错误!这是竞争条件。

并不是只有线程 7 在工作,而是所有线程都在引用内存中的同一个变量 i(只有一个副本!)所以自 数字 7 最后写入(大概在任何线程开始之前)它们都读取相同的 i==7.

试试这个 worker 函数,看看它是否能解决问题

def worker(num)
  my_thread_id = Thread.current.object_id

  while not @q.empty?
    puts "Thread: #{num} NumObjId: #{num.object_id} ThreadId: #{my_thread_id} Project: #{@q.pop}"
    sleep(0.5)
  end
end

注意 NumObjId 在所有线程中都是相同的。他们都指向同一个数字。但是我们得到的实际 ThreadId 是不同的。

如果您确实需要每个线程中的数字,请分配与线程一样多的数字。像

ids = (1..7).to_a
ids.each do |i|
  threads << Thread.new { worker(i) }
end