在 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
使用 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