Skip to content
Fibers and Concurrency
step 1/7

Reading — step 1 of 7

Learn

~3 min readClosures, Fibers, Refinements

Ruby has multiple concurrency primitives. Each has different trade-offs and use cases. The Pickaxe and Programming Ruby treat them across separate chapters.

Threads — preemptive, but GIL-limited

threads = []
5.times do |i|
  threads << Thread.new(i) do |n|
    sleep(rand * 0.1)
    puts "thread #{n}"
  end
end
threads.each(&:join)

MRI Ruby has the GIL (Global Interpreter Lock) — only one thread runs Ruby code at a time. CPU-bound work doesn't speed up with threads; I/O-bound work DOES, because the GIL releases during I/O.

JRuby and TruffleRuby don't have a GIL — true parallelism for them.

Fibers — cooperative, lightweight

fib = Fiber.new do
  a, b = 0, 1
  loop do
    Fiber.yield a
    a, b = b, a + b
  end
end

10.times { puts fib.resume }

Fibers SUSPEND at Fiber.yield and resume at the next .resume. Cooperative — they only yield when they explicitly choose to.

Key properties:

  • Tiny stack (~4 KB) — millions of fibers possible
  • No locking needed (cooperative)
  • Used to implement generators, async I/O

Generators with Enumerator

Fibers power Ruby's Enumerator:

squares = Enumerator.new do |y|
  i = 1
  loop { y << i*i; i += 1 }
 end

squares.next        # 1
squares.next        # 4
squares.first(5)    # [1, 4, 9, 16, 25]

Lazy infinite sequences. The block runs cooperatively, suspending after each y << value.

Async I/O with the fiber scheduler (Ruby 3+)

require 'async'

Async do
  3.times.map do |i|
    Async do
      sleep(0.1)
      puts "task #{i} done"
    end
  end.each(&:wait)
end

The async gem wraps fibers + a scheduler. Concurrent I/O without explicit thread management. Comparable to JavaScript's async/await pattern.

Ractors (Ruby 3+) — true parallelism

worker = Ractor.new do
  while msg = Ractor.receive
    Ractor.yield msg.upcase
  end
end

worker.send("hello")
puts worker.take         # "HELLO"

worker.send("world")
puts worker.take         # "WORLD"

Ractors are truly parallel — each runs independently, communicating only via message passing. Like Erlang processes, Go goroutines, Rust + threads with channels.

Constraints:

  • No shared mutable state — values shared between Ractors must be deeply frozen or shareable
  • Many existing libraries don't yet work in Ractors
  • Still considered experimental as of Ruby 3.x

Process-based parallelism

pids = []
4.times do |i|
  pids << fork do
    # in child process — separate memory, separate GIL
    perform_work(i)
  end
end
Process.waitall

Classic Unix fork. Each child has its own memory. Used by Sidekiq, Resque (background workers), Unicorn, Puma (cluster mode web servers).

When to use what

  • Threads — I/O-bound work (HTTP requests, DB queries) — GIL doesn't hurt because threads sleep during I/O
  • Fibers / async gem — many concurrent I/O operations on a single thread; lighter than threads
  • Ractors — CPU-parallel work in pure Ruby (when supported); experimental
  • Processes — heavy CPU work; classic background-job pattern

For most web app code, threads + connection pooling is fine. Reach for fibers/ractors when you've hit a real bottleneck.

Common mistakes

  • Expecting CPU speedup from threads in MRI Ruby — GIL prevents it.
  • Sharing mutable state between threads without locks — race conditions.
  • Forgetting Thread.abort_on_exception = true in dev — silent thread crashes.
  • Using fork in a multi-threaded process — cleanup is fragile; only the forking thread survives in the child.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…