Karl Hans Janke Kollaborativ
Heute die Welt, morgen das Sonnensystem!
<< prev next >>

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
    end

    def resume(*xs)
        @@fs.push(self)
        jump(xs)                    # jumping into fiber
    end

    def self.current
        @@fs.last
    end

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

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

Download: fibr.rb


Comments:

Szabo Peter, 2.12.2010 1:25:53

Hi,

FYI Ruby 1.8 has terrible random memory leaks when call/cc is used. For example, if I change

destination.call(*xs)

to

destination.call(*xs).class

in your Fibr implementation, I'll get a memory leak:

GC.enable
GC.start
Process.setrlimit(Process::RLIMIT_AS, 70 << 20)  # 70MB
(1..520000).each { |i|
  Fibr.new {}.object_id
  fail if 42 != Fibr.new { Fibr.yield 42 }.resume
  fail if nil != Fibr.new { 43; nil }.resume
  fail if nil != Fibr.new { 43; nil }.resume
  fail if 42 != Fibr.new { Fibr.yield 42; Fibr.yield 45 }.resume
}

Best regards,

Peter