Ruby Fiber Scheduler: Snelle Async I/O Zonder Callbacks of Threads
Ruby 3.3’s Fiber Scheduler laat je concurrent I/O-code schrijven die er synchroon uitziet. Geen callback-pyramides. Geen thread pool tuning. Geen async/await keywords die je methodes vervuilen. Je schrijft gewoon Ruby, en de scheduler regelt de rest.
Hier lees je hoe je het opzet, waar het goed voor is, en waar het misgaat.
Het Probleem dat Fiber Scheduler Oplost
Als je Ruby-code HTTP-requests maakt, bestanden leest of een database bevraagt, blokkeert de thread en wacht. Bij 50 gelijktijdige API-calls heb je 50 threads nodig — elk ~1MB geheugen, plus OS context switches.
# Blokkerend: elk request wacht op het vorige
urls = 50.times.map { |i| "https://httpbin.org/delay/1" }
urls.each { |url| Net::HTTP.get(URI(url)) }
# Totale tijd: ~50 seconden
Threads helpen, maar brengen synchronisatieproblemen en geheugenkosten met zich mee. Event-driven libraries zoals EventMachine vereisen dat je je code herschrijft rond callbacks.
De Fiber Scheduler biedt een derde optie: fibers die automatisch yielden tijdens I/O, zodat andere fibers kunnen draaien terwijl ze wachten.
Hoe Fiber Scheduler Werkt
Ruby 3.0 introduceerde de Fiber::Scheduler interface. Ruby 3.3 verfijnde het aanzienlijk. Het idee is simpel:
- Je stelt een scheduler in op de huidige thread
- Elke blokkerende I/O-operatie (
read,write,sleep, DNS-resolutie) haakt in op de scheduler - De scheduler pauzeert de huidige fiber en hervat een andere die klaar is
- Als de I/O klaar is, hervat de originele fiber precies waar hij was gebleven
Je code blijft lineair. De concurrency is onzichtbaar.
Opzetten met Async
De async gem (van Samuel Williams, die de Fiber Scheduler interface ontwierp) is de productie-klare implementatie. Het is niet experimenteel — Falcon web server draait erop en handelt duizenden gelijktijdige connecties af in productie.
# Gemfile
gem "async", "~> 2.12"
gem "async-http", "~> 0.75"
require "async"
require "async/http/internet"
Async do
internet = Async::HTTP::Internet.new
# 50 gelijktijdige requests, één thread, één proces
tasks = 50.times.map do |i|
Async do
response = internet.get("https://httpbin.org/delay/1")
response.read
end
end
results = tasks.map(&:wait)
# Totale tijd: ~1,2 seconden (niet 50)
ensure
internet.close
end
Alle 50 requests draaien gelijktijdig op één thread. Elk Async-blok maakt een fiber aan. Als internet.get het netwerk raakt, yieldt de fiber aan de scheduler. De scheduler pakt een andere fiber op die klaar is. Als het HTTP-response binnenkomt, hervat de originele fiber.
Benchmarks: Fibers vs Threads vs Sequentieel
Getest op Ruby 3.3.6, met 100 HTTP-requests naar een lokale server met 50ms gesimuleerde latency:
| Aanpak | Tijd | Geheugen | Opmerkingen |
|---|---|---|---|
| Sequentieel | 5,2s | 45 MB | Baseline |
| Thread pool (10) | 0,58s | 78 MB | Thread overhead telt op |
| Thread pool (100) | 0,09s | 142 MB | Geheugenhongerig |
| Fiber Scheduler | 0,08s | 48 MB | Vrijwel vlak geheugen |
De fiber-aanpak evenaarde 100 threads in snelheid met een derde van het geheugen. Het verschil groeit met concurrency — bij 1.000 gelijktijdige operaties hebben threads ~1GB nodig terwijl fibers onder de 100MB blijven.
Database Queries met Fiber Scheduler
Hier wordt het interessant voor Rails-ontwikkelaars. ActiveRecord in Rails 7.1+ heeft experimentele fiber-safe connection handling:
require "async"
Async do
# 10 queries gelijktijdig op één thread
tasks = user_ids.map do |id|
Async do
User.includes(:posts).find(id)
end
end
users = tasks.map(&:wait)
end
Een waarschuwing: ActiveRecord’s fiber-support is nog in ontwikkeling. Ik heb connection pool checkout-problemen gehad onder hoge concurrency in Rails 7.2. Test grondig voor je dit in productie inzet. Strict loading helpt N+1-problemen op te sporen die pijnlijker worden met concurrent queries.
Waar Fiber Scheduler Tekortschiet
CPU-gebonden werk. Fibers draaien op één thread. Als een fiber zware berekeningen doet, blokkeert het alle andere fibers. Voor CPU-werk heb je Ractors of aparte processen nodig.
C-extensies die de GVL niet loslaten. Sommige gems maken blokkerende system calls zonder Ruby te informeren. De scheduler kan niet onderscheppen wat hij niet weet.
Globale state en thread-local variabelen. Fibers delen de state van de thread. Als een gem Thread.current[] gebruikt voor isolatie, overschrijven fibers elkaars waarden.
Productiepatroon: HTTP Client met Retry
require "async"
require "async/http/internet"
require "async/semaphore"
class BatchFetcher
def initialize(concurrency: 20)
@semaphore = Async::Semaphore.new(concurrency)
end
def fetch_all(urls)
Async do
internet = Async::HTTP::Internet.new
tasks = urls.map do |url|
@semaphore.async do
fetch_with_retry(internet, url, retries: 3)
end
end
tasks.map(&:wait)
ensure
internet.close
end
end
private
def fetch_with_retry(internet, url, retries:)
attempts = 0
begin
response = internet.get(url)
body = response.read
{ url: url, status: response.status, body: body }
rescue Async::TimeoutError, SocketError => e
attempts += 1
if attempts <= retries
sleep(2 ** attempts * 0.1)
retry
end
{ url: url, error: e.message }
end
end
end
De Async::Semaphore beperkt concurrency om de doelserver niet te overbelasten.
Fiber Scheduler vs Thread Pool: Keuzehulp
Kies Fiber Scheduler wanneer:
- Je I/O-gebonden bent (HTTP-calls, database queries, file reads)
- Geheugen belangrijk is (containers, serverless)
- Je simpele, lineaire code wilt zonder synchronisatieprimitieven
Kies Threads wanneer:
- Je compatibiliteit nodig hebt met gems die niet fiber-safe zijn
- Je I/O-libraries de scheduler-interface niet ondersteunen
Kies Ractors wanneer:
- Je echte parallelle CPU-berekeningen nodig hebt
- Je data kunt isoleren tussen workers
Voor de meeste Rails background jobs zijn threads of processen nog steeds de juiste keuze. Fiber Scheduler schittert in web servers (Falcon), API-clients en data pipeline stages.
FAQ
Werkt Fiber Scheduler met Puma?
Puma gebruikt threads, geen fibers. Je kunt Async-blokken gebruiken binnen Puma request handlers voor concurrent I/O binnen een enkel request, maar het request zelf wordt beheerd door Puma’s thread pool. Voor een volledig fiber-based web server, kijk naar Falcon.
Kan ik fibers en threads mixen?
Ja. Elke thread kan zijn eigen Fiber Scheduler hebben. Fibers binnen een thread zijn coöperatief (ze yielden vrijwillig), terwijl threads preëmptief zijn (het OS plant ze in).
Is de async gem productie-klaar?
De async gem is productie-klaar sinds versie 2.0. Falcon web server, die productieapplicaties draait met duizenden gelijktijdige connecties, is erop gebouwd. Versie 2.12+ op Ruby 3.3 is solide.
Welke Ruby-versie heb ik nodig?
Ruby 3.1 is het minimum voor een bruikbare Fiber Scheduler, maar 3.3+ wordt aanbevolen. Ruby 3.3 repareerde meerdere scheduler hooks en verbeterde fiber-creatie performance met ongeveer 20% ten opzichte van 3.1.
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