Запуск многопоточного вызова Open3 в Ruby - PullRequest
0 голосов
/ 10 января 2019

У меня есть большой цикл, в котором я пытаюсь запустить вызов Open3.capture3 в потоках вместо линейного выполнения. Каждый поток должен работать независимо, и нет никаких тупиков с точки зрения доступа к данным.

Проблема в том, что многопоточная версия намного медленнее и загружает мой процессор.

Вот пример линейной программы:

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

1 Ответ

0 голосов
/ 10 января 2019

Каждый поток должен работать независимо, и нет никаких тупиков с точки зрения доступа к данным.

Вы уверены в этом?

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

Ваши темы делятся стандартным выводом. Если вы посмотрите на свой вывод, то увидите, что вы не получаете никакого чередующегося текстового вывода, потому что Ruby автоматически обеспечивает взаимное исключение на stdout, поэтому ваши потоки эффективно работают последовательно с кучей бесполезных конструкций / деконструкций / переключений тратить время.

Потоки в Ruby эффективны только для параллелизма, если вы вызываете какой-то контекст Rubyless *. Таким образом, виртуальная машина знает, что она может безопасно работать параллельно без взаимодействия потоков друг с другом. Посмотрите, что произойдет, если мы просто запишем вывод оболочки в потоках:

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.

...