Karl Hans Janke Kollaborativ
Heute die Welt, morgen das Sonnensystem!

jun 2010

Fibers for Ruby 1.8 in 42 lines feat. call/cc

Actually, stripped of comments and stuff, it's even less than 42 lines.

Fibers are a big new thing with Ruby 1.9. The name is supposed to suggest that they're the thinner things inside a thread. You create them with a block to execute but they won't run in parallel. Instead they are explicitly entered and left via resume and yield. The first call to resume enters at the top of the block. Calling yield from inside the block jumps back out and the next resume jumps back inside. Repeat as often as you like.

f = Fiber.new {
    Fiber.yield 23  # returns 5
f.resume            # start it up; returns 23
...                 # control transfers back here after "yield"
f.resume 5          # run the rest

The two can pass arguments to each other like regular procedure calls. So it's indeed like threads as there are independent control flows. But execution switches only explicitly and intercommunication is much more direct.

Today I found out what they are really useful for: To separate some very sequential business logic (do A, do B, do C, finish) from the event-centric tangle of a GUI toolkit. A fiber does A, B, and C in sequence. Where does means it starts the job in the background, registers an event handler, and immediately yields back to the GUI event loop. When the job finishes, the handler resumes the fiber and it goes on to the next step.

Ruby 1.9 adds fibers as a primitive, but they are also easily implemented in terms of call/cc - call with current continuation, which Ruby has had for no idea how long. So here goes, a drop-in substitute for 1.9's Fiber class:

class Fibr
    @@fs = []   # a stack of fibers corresponding to calls of 'resume'

    def initialize(&block)
        @k = lambda(&block)         # lambda makes 'return' work as expected

    def resume(*xs)
        jump(xs)                    # jumping into fiber

    def self.current

    def self.yield(*xs)
        f = @@fs.pop
        f && f.send(:jump, xs)      # jumping out of fiber

    def jump(xs)
        callcc { |k|
            destination = @k
            @k = k
            @k.call                 # return from the last 'resume'
Fiber = Fibr if RUBY_VERSION<"1.9"

Download: fibr.rb