Как работает GVL и как он влияет на threads в MRI Ruby?
GVL — мьютекс MRI, позволяющий только одному потоку выполнять Ruby-код одновременно; для IO-задач потоки эффективны (GVL отдаётся при ожидании), для CPU-параллелизма используйте процессы или Ractors.
GVL (Global VM Lock) в MRI Ruby
GVL (Global VM Lock), ранее называемый GIL (Global Interpreter Lock) — это мьютекс, встроенный в интерпретатор MRI (CRuby). Он гарантирует, что в любой момент времени только один поток Ruby выполняет Ruby-код. Остальные потоки ждут освобождения блокировки.
Как работает GVL
GVL удерживается потоком, пока он выполняет Ruby-инструкции. Поток освобождает GVL в двух случаях:
- При блокирующем системном вызове: ввод-вывод (read, write, accept), sleep, wait. В этот момент другой поток может захватить GVL и продолжить работу.
- По истечении кванта времени (~100 мс или ~1000 инструкций): интерпретатор принудительно переключает потоки через сигнал
RUBY_THREAD_TIMESLICE.
require 'benchmark'
require 'thread'
# CPU-bound: потоки не дают прироста из-за GVL
def cpu_work
10_000_000.times { |i| i * i }
end
sequential_time = Benchmark.realtime do
cpu_work
cpu_work
end
threaded_time = Benchmark.realtime do
t1 = Thread.new { cpu_work }
t2 = Thread.new { cpu_work }
t1.join
t2.join
end
puts "Sequential: #{sequential_time.round(2)}s"
puts "Threaded: #{threaded_time.round(2)}s"
# Результат почти одинаковый — GVL не даёт параллельности CPU
IO-bound задачи: потоки работают эффективно
Когда поток ожидает IO, он отдаёт GVL, и другие потоки продолжают работать. Поэтому для HTTP-запросов, работы с БД, файловых операций потоки дают реальный прирост.
require 'net/http'
require 'benchmark'
urls = Array.new(5) { "http://example.com" }
# IO-bound: потоки дают ~5x ускорение
threaded_time = Benchmark.realtime do
threads = urls.map do |url|
Thread.new do
Net::HTTP.get(URI(url))
end
end
threads.each(&:join)
end
puts "5 concurrent requests: #{threaded_time.round(2)}s"
# vs ~5x дольше при последовательном выполнении
Нативные C-расширения и GVL
C-расширения могут явно освобождать GVL через rb_thread_call_without_gvl(). Многие популярные гемы делают это для CPU-интенсивных операций: bcrypt, nokogiri (парсинг XML), некоторые драйверы БД. Это позволяет реальному параллелизму в C-коде при многопоточном Ruby.
Альтернативы для CPU-параллелизма
- Process.fork / multiprocessing: каждый процесс имеет собственный GVL. Puma в режиме cluster использует это.
- Ractors (Ruby 3.0+): новая модель параллелизма без GVL. Каждый Ractor изолирован, объекты между ними передаются через каналы или freeze. Пока экспериментальная, но работает без GVL.
- JRuby / TruffleRuby: не имеют GVL, реально параллельны в потоках.
# Ractors — параллельный CPU без GVL (Ruby 3.0+)
results = (1..4).map do |i|
Ractor.new(i) do |n|
# Реально параллельно — нет GVL!
sum = 0
(n * 1_000_000).times { |j| sum += j }
sum
end
end.map(&:take)
puts results.sum
Thread safety при GVL
GVL не защищает от состояний гонки в Ruby! Он гарантирует, что Ruby-инструкции атомарны на уровне C, но составные операции (read-modify-write) всё равно не потокобезопасны без мьютекса.
counter = 0
threads = 100.times.map do
Thread.new { 1000.times { counter += 1 } } # race condition!
end
threads.each(&:join)
puts counter # не 100_000! Гонка данных.
# Правильно:
mutex = Mutex.new
counter = 0
threads = 100.times.map do
Thread.new { 1000.times { mutex.synchronize { counter += 1 } } }
end
threads.each(&:join)
puts counter # 100_000
Подводные камни
- GVL не защищает от race conditions: составные операции типа
hash[key] ||= []не атомарны — используйтеMutexилиConcurrent::Mapиз concurrent-ruby. - CPU-bound потоки бесполезны в MRI: добавление потоков для вычислений не даёт ускорения. Используйте процессы (fork) или Ractors.
- Дедлоки:
Thread#join+ неверная очерёдность мьютексов = дедлок. ИспользуйтеMutex#try_lockили задавайте таймаут. - Thread.raise небезопасен: вызов
thread.raiseизвне может прервать поток в любой точке, включаяensure-блоки. Предпочитайте совместную отмену через флаги илиQueue. - Fiber vs Thread: Fiber не переключаются автоматически — это кооперативная многозадачность. Fiber::Scheduler (Ruby 3.0+) позволяет неблокирующий IO без потоков.
- ObjectSpace не потокобезопасен: не итерируйте
ObjectSpaceв многопоточном коде без остановки GC.
Common mistakes
- Отвечать определением без production-сценария.
- Не называть runtime boundary, security boundary или failure mode.
- Игнорировать версию API, observability и тестовую проверку.
What the interviewer is testing
- Объясняет механизм своими словами и без выдуманных API.
- Называет реальные риски, диагностику и критерий корректности.
- Связывает ответ с текущей документацией и миграционными ограничениями.