Celluloid Futures 不比同步计算快吗?
Celluloid Futures not faster than synchronous computation?
我正在尝试使用 Celluloid 异步处理一些 .csv 数据。我读过使用 futures 可以让你在主线程终止之前等待一组参与者完成。我看过 some examples 证明了这一点。
但是,当我在我的示例代码中实现它时,事实证明使用 futures 并不比同步处理快多少。谁能看到我做错了什么?
require 'smarter_csv'
require 'celluloid/current'
require 'benchmark'
class ImportActor
include Celluloid
def process_row(row)
100000.times {|n| n}
end
end
def do_all_the_things_with_futures
pool = ImportActor.pool(size: 10)
SmarterCSV.process("all_the_things.csv").map do |row|
pool.future(:process_row,row)
end.map(&:value)
end
def do_all_the_things_insync
pool = ImportActor.pool(size: 10)
SmarterCSV.process("all_the_things.csv") do |row|
pool.process_row(row)
end
end
puts Benchmark.measure { do_all_the_things_with_futures}
puts Benchmark.measure { do_all_the_things_insync }
2.100000 0.030000 2.130000 ( 2.123381)
2.060000 0.020000 2.080000 ( 2.069357)
[4.6 秒内完成]
您使用的是标准 ruby MRI 解释器吗?
如果是这样,您将不会对完全 CPU 绑定的任务获得任何加速 -- 即,不执行任何 I/O 但完全执行计算的任务在 CPU。 100000.times {|n| n}
的 'test' 任务确实完全受 CPU 限制。
对于 MRI 上完全 CPU 绑定的任务,您无法通过多线程获得任何加速的原因是因为 MRI 解释器具有 "Global Interpreter Lock" (GIL),即防止 ruby 解释器同时使用多个 CPU 内核。多线程并行性,就像赛璐珞给你的那样,只能通过在不同的 CPU 内核上同时 运行 不同的线程来加快 CPU 的工作,在像大多数系统这样的多核系统上,这些是这些天。
但在 MRI 中,这是不可能的。这是 ruby MRI 解释器的局限性。
如果您安装了 JRuby 并且 运行 您的测试是在 JRuby 下进行的,您应该会看到加速。
如果您的任务涉及一些 I/O(例如进行数据库查询,或等待远程 HTTP API,或执行 大量 文件阅读或写作),您还可以在 MRI 下看到一些加速。您的任务花费 I/O 的时间越多,加速就越快。这是因为即使 MRI 不允许线程在多个 CPU 核心上同时执行,等待 I/O 的线程仍然可以被切换出去,另一个线程被切换进来做工作。而如果您不使用线程,程序将只是等待 I/O 不做任何工作。
如果您 google 支持 "ruby GIL",您可以找到更多关于该问题的讨论。
如果您确实在做 CPU 密集型工作,并且可以从多线程并行性中获益,从而显着帮助您的程序,请考虑切换到 Jruby。
如果您确实需要多线程并行性,使用 Celluloid 的替代方法是使用 concurrent-ruby 包中的 Futures 或 Promises。 Concurrent-ruby 通常比 Celluloid 内部更简单,重量更轻。然而,无论您使用哪种工具,编写多线程代码都可能很棘手,即使您使用 Celluloid 或 ruby-concurrent 来为您提供比直接使用线程更好的更高级别的抽象,使用多线程并发将需要熟悉一些技术,并且不时需要一些棘手的调试。
我正在尝试使用 Celluloid 异步处理一些 .csv 数据。我读过使用 futures 可以让你在主线程终止之前等待一组参与者完成。我看过 some examples 证明了这一点。
但是,当我在我的示例代码中实现它时,事实证明使用 futures 并不比同步处理快多少。谁能看到我做错了什么?
require 'smarter_csv'
require 'celluloid/current'
require 'benchmark'
class ImportActor
include Celluloid
def process_row(row)
100000.times {|n| n}
end
end
def do_all_the_things_with_futures
pool = ImportActor.pool(size: 10)
SmarterCSV.process("all_the_things.csv").map do |row|
pool.future(:process_row,row)
end.map(&:value)
end
def do_all_the_things_insync
pool = ImportActor.pool(size: 10)
SmarterCSV.process("all_the_things.csv") do |row|
pool.process_row(row)
end
end
puts Benchmark.measure { do_all_the_things_with_futures}
puts Benchmark.measure { do_all_the_things_insync }
2.100000 0.030000 2.130000 ( 2.123381)
2.060000 0.020000 2.080000 ( 2.069357)
[4.6 秒内完成]
您使用的是标准 ruby MRI 解释器吗?
如果是这样,您将不会对完全 CPU 绑定的任务获得任何加速 -- 即,不执行任何 I/O 但完全执行计算的任务在 CPU。 100000.times {|n| n}
的 'test' 任务确实完全受 CPU 限制。
对于 MRI 上完全 CPU 绑定的任务,您无法通过多线程获得任何加速的原因是因为 MRI 解释器具有 "Global Interpreter Lock" (GIL),即防止 ruby 解释器同时使用多个 CPU 内核。多线程并行性,就像赛璐珞给你的那样,只能通过在不同的 CPU 内核上同时 运行 不同的线程来加快 CPU 的工作,在像大多数系统这样的多核系统上,这些是这些天。
但在 MRI 中,这是不可能的。这是 ruby MRI 解释器的局限性。
如果您安装了 JRuby 并且 运行 您的测试是在 JRuby 下进行的,您应该会看到加速。
如果您的任务涉及一些 I/O(例如进行数据库查询,或等待远程 HTTP API,或执行 大量 文件阅读或写作),您还可以在 MRI 下看到一些加速。您的任务花费 I/O 的时间越多,加速就越快。这是因为即使 MRI 不允许线程在多个 CPU 核心上同时执行,等待 I/O 的线程仍然可以被切换出去,另一个线程被切换进来做工作。而如果您不使用线程,程序将只是等待 I/O 不做任何工作。
如果您 google 支持 "ruby GIL",您可以找到更多关于该问题的讨论。
如果您确实在做 CPU 密集型工作,并且可以从多线程并行性中获益,从而显着帮助您的程序,请考虑切换到 Jruby。
如果您确实需要多线程并行性,使用 Celluloid 的替代方法是使用 concurrent-ruby 包中的 Futures 或 Promises。 Concurrent-ruby 通常比 Celluloid 内部更简单,重量更轻。然而,无论您使用哪种工具,编写多线程代码都可能很棘手,即使您使用 Celluloid 或 ruby-concurrent 来为您提供比直接使用线程更好的更高级别的抽象,使用多线程并发将需要熟悉一些技术,并且不时需要一些棘手的调试。