运行 Ruby 中的多线程 Open3 调用

Running multi-threaded Open3 call in Ruby

我有一个大循环,我试图 运行 在线程中调用 Open3.capture3 而不是 运行 线性调用。每个线程应该 运行 独立并且在访问数据方面没有死锁。

问题是,线程版本太慢了,它占用了我的 CPU。

这里有一个线性规划的例子:

require 'open3'

def read(i)
  text, _, _ = Open3.capture3("echo Hello #{i}")
  text.strip
end

(1..400).each do |i|
  puts read(i)
end

这是线程版本:

require 'open3'
require 'thread'

def read(i)
  text, _, _ = Open3.capture3("echo Hello #{i}")
  text.strip
end

threads = []
(1..400).each do |i|
  threads << Thread.new do
    puts read(i)
  end
end

threads.each(&:join)

A时间比较:

$ time ruby linear.rb
ruby linear.rb  0.36s user 0.12s system 110% cpu 0.433 total
------------------------------------------------------------
$ time ruby threaded.rb 
ruby threaded.rb  1.05s user 0.64s system 129% cpu 1.307 total

Each thread should run independently and there's no deadlock in terms of accessing data.

你确定吗?

threads << Thread.new do
  puts read(i)
end

您的主题正在共享标准输出。如果你查看你的输出,你会发现你没有得到任何交错的文本输出,因为 Ruby 自动确保 stdout 上的互斥,所以你的线程有效地 运行 与一群没用的 construction/deconstruction/switching 浪费时间。

Ruby 中的线程仅在调用某些 Ruby 较少的上下文* 时才对并行有效。这样 VM 就知道它可以安全地 运行 并行,而线程不会相互干扰。看看如果我们只捕获线程中的 shell 输出会发生什么:

threads = Array.new(400) { |i| Thread.new { `echo Hello #{i}` } }
threads.each(&:join)
# time: 0m0.098s

与连续

output = Array.new(400) { |i| `echo Hello #{i}` }
# time: 0m0.794s

* 事实上,这取决于几个因素。一些虚拟机 (JRuby) 使用本机线程,并且更容易并行化。某些 Ruby 表达式比其他表达式更可并行化(取决于它们与 GVL 的交互方式)。确保并行性的最简单方法是 运行 单个外部命令,例如子进程或系统调用,这些通常是无 GVL 的。