Ruby Ractors: Echte Parallelle Verwerking Zonder de GVL
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.
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 TouchRelated Articles
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