35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English 35+ Years Experience Netherlands Based ⚡ Fast Response Times Ruby on Rails Experts AI-Powered Development Fixed Pricing Available Senior Architects Dutch & English
Ruby Ractors: Echte Parallelle Verwerking Zonder de GVL

Ruby Ractors: Echte Parallelle Verwerking Zonder de GVL

TTB Software
ruby
Hoe je Ruby Ractors gebruikt voor parallelle CPU-gebonden taken in Ruby 3.3+. Praktische voorbeelden, benchmarks en valkuilen uit productie.

Ruby Ractors bieden echte parallelle executie op meerdere CPU-cores — iets wat Threads niet kunnen door de Global VM Lock (GVL, voorheen GIL). Als je CPU-intensief werk hebt en nog steeds naar Sidekiq of aparte processen grijpt, zijn Ractors misschien precies wat je nodig hebt.

Hier lees je hoe ze werken, waar ze falen, en wanneer ze de moeite waard zijn in Ruby 3.3.

Wat het GVL-Probleem Eigenlijk Is

Ruby threads zijn concurrent maar niet parallel voor CPU-werk. De GVL zorgt ervoor dat slechts één thread tegelijk Ruby-code uitvoert. Threads helpen wel bij I/O-gebonden werk (netwerkverzoeken, bestandslezen) omdat de GVL wordt losgelaten tijdens I/O-waits. Maar als je getallen moet crunchen, grote datasets moet parsen of beeldverwerking doet — threads geven je nul speedup.

# Dit wordt NIET sneller met threads (CPU-gebonden)
results = 4.times.map do
  Thread.new { (1..10_000_000).reduce(:+) }
end.map(&:value)

Elke thread neemt om de beurt de GVL over. Vier threads, één core. Je betaalt thread-overhead voor sequentiële executie.

Ractors: De Oplossing

Ractors (Ruby Actors) draaien in geïsoleerde geheugenruimtes, elk met hun eigen GVL. Dit betekent dat ze daadwerkelijk tegelijkertijd op aparte CPU-cores draaien.

# Dit draait WEL parallel
ractors = 4.times.map do
  Ractor.new do
    (1..10_000_000).reduce(:+)
  end
end

results = ractors.map(&:take)

Op een 4-core machine met Ruby 3.3.6 mat ik het volgende:

Aanpak Tijd (seconden) Speedup
Sequentieel 2,41 1x
4 Threads 2,38 ~1x
4 Ractors 0,64 3,8x

Bijna lineaire schaling. Dat is echte parallelisme.

De Isolatieregel

Ractors bereiken thread-safety door gedeelde mutable state te verbieden. Elke Ractor krijgt zijn eigen heap. Je communiceert tussen Ractors door berichten te sturen en te ontvangen — niet door objecten te delen.

# Dit werkt — een waarde versturen
r = Ractor.new do
  name = Ractor.receive
  "Hallo, #{name}"
end

r.send("Ruby")
puts r.take  # => "Hallo, Ruby"
# Dit FAALT — mutable state delen
shared_array = [1, 2, 3]
Ractor.new(shared_array) do |arr|
  arr << 4  # Ractor::IsolationError
end

Objecten die naar een Ractor worden gestuurd worden ofwel verplaatst (de verzender verliest toegang) of gekopieerd (deep copy via Marshal). Bevroren objecten en deelbare objecten (integers, symbols, frozen strings) kunnen worden gedeeld zonder kopiëren.

Praktisch Voorbeeld: Parallelle CSV-Verwerking

Stel je hebt een 2GB CSV-bestand en moet aggregaten berekenen. Het werk verdelen over Ractors:

require 'csv'

lines = File.readlines('transactions.csv')
header = lines.shift
chunk_size = lines.size / 4

ractors = 4.times.map do |i|
  chunk = lines[i * chunk_size, chunk_size] || []
  Ractor.new(chunk) do |data|
    total = 0
    count = 0
    data.each do |line|
      cols = line.split(',')
      amount = cols[3].to_f
      total += amount
      count += 1
    end
    { total: total, count: count }
  end
end

results = ractors.map(&:take)
grand_total = results.sum { |r| r[:total] }
grand_count = results.sum { |r| r[:count] }

puts "Verwerkt: #{grand_count} records, totaal: #{grand_total}"

Merk op dat ik het bestand lees in de hoofd-Ractor en string-chunks verstuur. Je kunt geen File-handle of CSV::Row-objecten naar een Ractor sturen omdat ze niet deelbaar zijn.

Waar Ractors Tekort Schieten

De meeste gems werken niet in Ractors. Elke gem die global state, class variables of mutable constants gebruikt, gooit een Ractor::IsolationError. Rails is compleet incompatibel met Ractors — ActiveRecord, ActiveSupport, niets ervan werkt in een Ractor.

Debuggen is lastig. Stack traces van Ractor-crashes zijn niet altijd duidelijk. Een Ractor::RemoteError wikkelt de originele exceptie in, en je moet .cause aanroepen voor de echte fout.

Startup-kosten zijn reëel. Een Ractor aanmaken is zwaarder dan een Thread. Voor triviale workloads eten de kosten de winst op. In mijn benchmarks versloegen Ractors threads pas wanneer elk werkpakket minstens 10-50ms duurt.

De API is nog experimenteel. De core team heeft bevestigd dat Ractors in Ruby’s toekomst blijven, maar de API kan veranderen tussen versies.

Ractor Pools voor Hergebruik

Ractors per taak aanmaken is verspilling voor herhaalde korte taken. Bouw een pool:

pool_size = 4
pipe = Ractor.new do
  loop do
    Ractor.yield(Ractor.receive)
  end
end

workers = pool_size.times.map do
  Ractor.new(pipe) do |input|
    loop do
      task = input.take
      result = task.call
      Ractor.yield(result)
    end
  end
end

# Werk indienen
20.times do |i|
  pipe.send(-> { i * i })
end

# Resultaten verzamelen
20.times do
  _ractor, result = Ractor.select(*workers)
  puts result
end

Wanneer Ractors vs Alternatieven

Gebruik Ractors wanneer:

  • CPU-gebonden werk dat in onafhankelijke stukken gesplitst kan worden
  • Je alle code beheert (geen gem-afhankelijkheden in de Ractor)
  • Het werk per chunk de ~1ms Ractor-aanmaakkosten rechtvaardigt

Gebruik Threads wanneer:

  • I/O-gebonden werk (HTTP-calls, database queries, bestandsoperaties)
  • Je toegang nodig hebt tot gems en shared libraries
  • Background jobs in Rails je concurrency-behoeften afhandelen

Gebruik processen (fork of Sidekiq) wanneer:

  • Je volledige Rails-context nodig hebt in parallelle workers
  • Isolatie ook geheugenprotectie tegen segfaults moet omvatten

FAQ

Kan ik ActiveRecord gebruiken in een Ractor?

Nee. ActiveRecord leunt zwaar op class-level mutable state (connection pools, query caches, schema caches) die de Ractor-isolatieregels schenden. Als je database-toegang nodig hebt in parallel, gebruik Threads of aparte processen.

Hoeveel Ractors moet ik aanmaken?

Stem af op je CPU-core count voor CPU-gebonden werk. Meer Ractors dan cores aanmaken voegt scheduling-overhead toe zonder speedup. Gebruik Etc.nprocessors om beschikbare cores te detecteren.

Zijn Ractors productie-klaar?

Ze zijn stabiel genoeg voor specifieke use cases — data processing pipelines, rekenwerkers, parallel parsen. Maar de experimentele waarschuwing betekent dat je je Ruby-versie moet pinnen en voorbereid moet zijn op API-wijzigingen bij upgrades.

Vervangen Ractors de GVL volledig?

Onwaarschijnlijk. De GVL bestaat om C-extensies en interne VM-state te beschermen. Het verwijderen zou elke native gem breken. In plaats daarvan geeft elke Ractor zijn eigen GVL, waarmee het probleem wordt omzeild voor code die binnen isolatie-beperkingen kan leven.

T

About the Author

Roger Heykoop is een senior Ruby on Rails ontwikkelaar met 19+ jaar Rails ervaring en 35+ jaar ervaring in softwareontwikkeling. Hij is gespecialiseerd in Rails modernisering, performance optimalisatie, en AI-ondersteunde ontwikkeling.

Get in Touch

Share this article

Need Expert Rails Development?

Let's discuss how we can help you build or modernize your Rails application with 19+ years of expertise

Schedule a Free Consultation