Трудно сказать, почему именно вы не видите преимуществ многопоточности.Но вот мое предположение.
Допустим, у вас есть действительно интенсивный метод Ruby, запуск которого занимает 10 секунд и называется do_work
.И, что еще хуже, вам нужно запустить этот метод 100 раз.Вместо того, чтобы ждать 1000 секунд, вы можете попробовать многопоточность.Это может разделить работу между ядрами вашего процессора, сократив вдвое или, возможно, даже на четверть времени выполнения:
Array.new(100) { Thread.new { do_work } }.each(&:join)
Но нет, это, вероятно, все еще займет 1000 секунд, чтобы закончить.Почему?
Глобальная блокировка виртуальной машины
Рассмотрим этот пример:
thread1 = Thread.new { class Foo; end; Foo.new }
thread2 = Thread.new { class Foo; end; Foo.new }
Создание класса в Ruby делает много вещей под капотом, например, он долженсоздать реальный объект класса и назначить указатель этого объекта на глобальную константу (в некотором порядке).Что произойдет, если thread1 зарегистрирует эту глобальную константу, получит на полпути , создав реальный объект класса, и затем thread2 начнет работать, сказав: «О, Foo
уже существует. Давайте продолжим и запустим Foo.new
».Что происходит, так как класс не был полностью определен?Или что, если оба thread1 и thread2 создают новый объект класса, а затем оба пытаются зарегистрировать свой класс как Foo
?Кто победит?А как насчет объекта класса, который был создан и сейчас не зарегистрирован?
Официальное решение Ruby для этого простое: на самом деле не запускайте этот код параллельно.Вместо этого существует один массивный мьютекс, называемый «глобальной блокировкой виртуальной машины», который защищает все, что изменяет состояние виртуальной машины Ruby (например, создание класса).Таким образом, хотя два указанных выше потока могут чередоваться различными способами, виртуальная машина не может оказаться в недопустимом состоянии, поскольку каждая операция виртуальной машины является по существу атомарной.
Пример
Это занимает около 6секунд на моем ноутбуке:
def do_work
Array.new(100000000) { |i| i * i }
end
Это занимает около 18 секунд, очевидно
3.times { do_work }
Но это также занимает около 18, потому что GVL предотвращает фактический запуск потоков впараллель
Array.new(3) { Thread.new { do_work } }.each(&:join)
Это также занимает 6 секунд для запуска
def do_work2
sleep 6
end
Но теперь это также занимает около 6 секунд для запуска:
Array.new(3) { Thread.new { do_work2 } }.each(&:join)
Почему?Если вы покопаетесь в исходном коде Ruby, вы обнаружите, что sleep
в конечном счете вызывает функцию C native_sleep
, а в там мы видим
GVL_UNLOCK_BEGIN(th);
{
//...
}
GVL_UNLOCK_END(th);
.Разработчики Ruby знают, что sleep
не влияет на состояние виртуальной машины, поэтому они явно разблокировали GVL, чтобы позволить ему работать параллельно.Может быть непросто выяснить, что именно блокирует / разблокирует GVL, и когда вы собираетесь увидеть выигрыш в производительности.
Как исправить свой код
Я думаю, что-тов вашем коде есть GVL, поэтому, хотя некоторые части ваших потоков работают параллельно (как правило, любые подпроцессы / PTY), между ними в виртуальной машине Ruby все еще существует конфликт, заставляющий некоторые части сериализоваться.
ВашЛучший способ получить действительно параллельный код Ruby - это упростить его до чего-то вроде этого:
Array.new(x) { Thread.new { do_work } }
, где вы уверены, что do_work
- это нечто простое, которое определенно разблокирует GVL, например, порождает подпроцесс.Вы можете попробовать переместить ваш код Truecrypt в небольшой сценарий оболочки, чтобы Ruby больше не приходилось с ним взаимодействовать, когда он начал работать.
Я рекомендую начать с небольшого теста, который просто запускает несколько подпроцессов, иубедитесь, что они действительно работают параллельно, сравнив время с последовательным запуском.