Ruby: Ractor и Ruby 3.4 — наконец-то настоящая параллельность?

Ruby 3+ представил революционный механизм Ractor, который наконец-то позволяет достичь настоящей параллельности в MRI Ruby, обходя ограничения GIL. В статье разберём, как работают Ractor’ы в Ruby 3.4, какие объекты можно передавать между ними и как избежать типичных ошибок при работе с этим мощным инструментом для CPU-bound задач.


В MRI Ruby потоки не дают настоящей параллельности из-за GIL.
Но Ruby 3+ принёс новую надежду — Ractor. А в 3.4 стало ещё лучше.

🧬 Что такое Ractor?

Ractor (Ruby Actor) — механизм, позволяющий запускать несколько Ruby-интерпретаторов параллельно.

  • Каждому Ractor — своя память
  • Общение только через сообщения
  • Нет GIL между Ractor’ами

🚀 Пример: параллельное вычисление

r1 = Ractor.new { Ractor.yield 1 + 2 }
r2 = Ractor.new { Ractor.yield 3 + 4 }

puts r1.take  # => 3
puts r2.take  # => 7

Это выполняется параллельно.


⚠️ Но есть нюанс: объект должен быть “shareable”

r = Ractor.new do
  puts "Я получил: #{Ractor.receive}"
end

r.send("hello") # 💥 Ractor::Error: can’t send non-shareable object

🧊 Что можно передавать?

  • Immutable объекты: true, false, nil, числа, символы, frozen-строки
  • Object.new.freeze — работает
  • Массив из shareable-элементов — тоже работает
Ractor.new do
  puts Ractor.receive.inspect
end.send([1, 2, 3].freeze)  # => [1, 2, 3]

🆕 Ruby 3.4 — что нового для Ractor?

  • 📦 make_shareable(obj) — безопасно делает объект пригодным для передачи:

    obj = {a: 1}
    Ractor.make_shareable(obj)
    
  • ✅ Улучшена поддержка Symbol, Struct, Hash
  • 🔥 Новое поведение ошибок внутри Ractor — можно ловить Ractor::RemoteError
  • 📈 Более стабильная производительность при spawn и join

🧪 Коммуникация между Ractor’ами

r = Ractor.new do
  msg = Ractor.receive
  Ractor.yield "Привет, #{msg}"
end

r.send("Ruby")
puts r.take  # => Привет, Ruby

Важно: передача односторонняя. Для двусторонней — передавай Ractor.current:

r = Ractor.new do
  from = Ractor.receive
  from.send("👋 Привет из Ractor")
end

r.send(Ractor.current)
puts Ractor.receive

🧠 Где это применимо?

  • Обработка CPU-bound задач (шифрование, компрессия)
  • Параллельная сериализация/десериализация
  • Safe background processing без fork
  • Безопасный runtime для eval, sandbox

🔥 Частые ошибки

r = Ractor.new { puts "ok" }
puts r.class  # => Ractor

r.send(Object.new)  # 💥 Ractor::Error: can’t send non-shareable object

📌 Используй .freeze, Ractor.make_shareable, или работай через JSON.


🔚 Вывод: Ractor — первая настоящая многопоточность в MRI Ruby. В 3.4 — уже пригоден к жизни. Да, есть ограничения, но если тебе нужно реально параллельно — теперь есть способ.

🗓 Дата публикации: 09.03.2025, но это не точно...

Ruby Ractor MRI GIL параллельность CPU-bound задачи