RubySeniorTechnical
Как Ruby обрабатывает параллелизм? Что такое Threads, Fibers и GIL?
MRI Ruby имеет GVL (Global VM Lock), который не позволяет нескольким потокам выполнять Ruby-код одновременно. Thread полезен для I/O-bound задач. Fiber — кооперативная корутина для неблокирующего I/O. Ractor (Ruby 3) обходит GVL для CPU-bound параллелизма.
GVL / GIL — Global VM Lock
В MRI (CRuby) существует Global VM Lock (раньше называли GIL). Это мьютекс, который гарантирует, что только один поток Ruby выполняет VM-инструкции в любой момент времени. GVL защищает внутренние структуры данных интерпретатора от гонок.
Следствия:
- CPU-bound задачи не ускоряются от многопоточности в MRI — потоки не выполняются параллельно.
- I/O-bound задачи (HTTP, файлы, БД) ускоряются — при ожидании I/O GVL временно освобождается, другой поток получает управление.
- JRuby и TruffleRuby не имеют GVL — там Thread даёт настоящий параллелизм.
Thread
# Параллельные HTTP-запросы (I/O-bound — GVL снимается)
require 'net/http'
require 'uri'
urls = [
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/1"
]
start = Time.now
threads = urls.map do |url|
Thread.new do
uri = URI.parse(url)
Net::HTTP.get_response(uri)
end
end
results = threads.map(&:value) # value ждёт завершения и возвращает результат
puts "#{results.size} requests in #{Time.now - start:.2f}s" # ~1s вместо ~3s
Thread-безопасность
# Гонка данных (unsafe)
counter = 0
threads = 100.times.map { Thread.new { counter += 1 } }
threads.each(&:join)
puts counter # может быть не 100!
# Безопасно через Mutex
counter = 0
mutex = Mutex.new
threads = 100.times.map do
Thread.new { mutex.synchronize { counter += 1 } }
end
threads.each(&:join)
puts counter # => 100
Fiber — кооперативные корутины
Fiber — лёгкая корутина, переключается вручную через Fiber.yield/resume. Не создаёт поток ОС.
# Генератор с Fiber
fib = Fiber.new do
a, b = 0, 1
loop do
Fiber.yield a
a, b = b, a + b
end
end
10.times { print "#{fib.resume} " }
# => 0 1 1 2 3 5 8 13 21 34
Fiber::Scheduler (Ruby 3.0+) — неблокирующий I/O
Хук Fiber::Scheduler позволяет реализовать event loop: обычный код с sleep, read, write автоматически становится неблокирующим внутри Fiber:
require 'async' # гем: gem 'async'
Async do
task1 = Async { sleep 1; "task1" }
task2 = Async { sleep 1; "task2" }
puts [task1.wait, task2.wait].inspect
# Выполняется за ~1s, не за ~2s
end
Ractor (Ruby 3) — настоящий параллелизм
# Параллельные вычисления без GVL
ractors = 4.times.map do |i|
Ractor.new(i) { |id| id * id }
end
results = ractors.map(&:take) # => [0, 1, 4, 9]
Сравнение примитивов
- Thread: вытесняющий, OS-поток, GVL у MRI. I/O-bound задачи, фоновые операции.
- Fiber: кооперативный, зелёный поток, явное переключение. Генераторы, event loop, async I/O.
- Ractor: OS-поток, без GVL, изолированная память. CPU-bound параллелизм.
Подводные камни
- Uncaught exception в Thread молча убивает поток (по умолчанию). Устанавливайте
Thread.abort_on_exception = trueили вызывайтеthread.join/thread.valueдля получения исключения. - Мьютекс нельзя захватить дважды из одного потока (
Mutex#synchronizeне реентерабельный) — дедлок. - Thread-local переменные (
Thread.current[:key]) не видны в дочерних потоках — легко пропустить. - Fiber не может быть возобновлён из другого потока (до Ruby 3 с Fiber::Scheduler) — не пытайтесь шарить Fiber между потоками.
- GVL освобождается при нативных I/O-вызовах, но некоторые C-расширения держат его дольше — профилируйте с
GVL Profilerилиruby-gvl-profiler. - В Puma (многопоточный сервер) один запрос = один Thread; в Falcon — один Fiber. Понимайте модель конкурентности своего сервера.
- ActiveRecord не безопасен для использования одного Connection из нескольких потоков — connection pool и
with_connectionобязательны. - Дедлоки между мьютексами труднее диагностировать в Ruby, чем в Go — используйте
Thread.list.each(&:backtrace)для анализа зависших потоков.
Common mistakes
- Сводить threads fibers gvl к названию метода без lifecycle и failure path.
- Игнорировать модель runtime: объектная динамическая модель Ruby, где почти всё является объектом, а методы ищутся через ancestor chain.
- Не отделять validation, authorization, transaction boundary и business logic.
What the interviewer is testing
- Объясняет threads fibers gvl через конкретную точку lifecycle в Ruby.
- Приводит корректный минимальный пример без вымышленных методов или callbacks.
- Называет edge cases: пустые значения, ошибки, транзакции, безопасность или concurrency.