在 Ruby 哈希中并行创建键值对的问题

Issues with parallelizing the creation of key-value pairs in a Ruby hash

使用 Ruby 并使用 Parallel 和 JRuby 1.7.19 编写以下代码以加速从具有多个值的数组创建散列:

hash = {}
array = [
  {"id" => "A001", "value" => 1},
  {"id" => "B002", "value" => 0},
  {"id" => "C003", "value" => 3},
  {"id" => "D004", "value" => 0}]

Parallel.each(array, { in_threads: 5 }) do |item|
  if keep_item?(item)
    hash[item["id"]] = item
  end
end

def keep_item?(item)
  item["value"] > 0
end

我注意到在 Ruby 中向哈希并行添加键可能存在问题。此代码是否存在任何风险(线程安全、数据丢失、我不知道的奇怪锁等),以至于我应该将其保留为常规系列 #each 调用?

Hash 不是线程安全的。如果 keep_item? 访问 hash,就会出现竞争条件。即使没有,也有对 hash 的并发更新,这很容易出错。

如果没有锁或其他同步,理论上无法保证一个线程上对非线程安全 hash 的更新在另一个线程上可见。 hash 没有同步的并发更新可能会丢失数据,或者导致其他奇怪的问题。这取决于 Ruby Hash.

的实现

你的数据很简单,用普通的方式处理就可以了each。如果使用 Parallel,并为线程安全访问添加 mutex/lock,同步开销将显着增加整个过程的额外时间成本。安全并行版本可能会使用更多时间。

Parallel 当您的任务是 IO 有界或 CPU 有界时很有用,只要您有空闲核心并且任务不需要在彼此之间交换数据.

虽然 Arie Shaw 关于 Hash 不是线程安全的说法是正确的,但我认为您的实现才是最重要的问题。如果您必须并行处理,我建议您使用 Parallel::map 代替,例如

 #lambda just to create the array structure
 lam = ->(n) {n.times.map {|i| {'id' => 'A' + i.to_s, 'value' => [i,0].shuffle.pop }}}
 a = lam.call(40_000)
 require 'parallel'
 Parallel.map(a,in_threads: 5) {|h| [h['id'],h] if h['value'] > 0}.compact.to_h

请注意,这比 运行 直接针对数组要慢得多,例如

a.map {|h| [h['id'],h] if h['value'] > 0}.compact.to_h

fruity 结果:

require 'fruity'
compare do
  in_parallel { Parallel.map(a, in_threads:5){|h| [h["id"],h] if h["value"] > 0}.compact.to_h }
  standard_array { a.map{|h| [h["id"],h] if h["value"] > 0}.compact.to_h }
end

#Running each test 64 times. Test will take about 6 minutes.
# standard_array is faster than in_parallel by 5x ± 1.0