Skip to content

Instantly share code, notes, and snippets.

@alskipp
Created January 5, 2012 14:05
Show Gist options
  • Save alskipp/1565393 to your computer and use it in GitHub Desktop.
Save alskipp/1565393 to your computer and use it in GitHub Desktop.
Macruby Fibers using GCD (transfer method implemented, 'double resume' error needs implementing)
class FiberError < StandardError; end
class Fiber
attr_accessor :name
def initialize &block
raise ArgumentError, 'new Fiber requires a block' unless block_given?
@block = block
@yield_sem = Dispatch::Semaphore.new(0)
@resume_sem = Dispatch::Semaphore.new(0)
@fiber_queue = Dispatch::Queue.new "#{__id__}"
@fiber_queue.async {@yield_sem.wait}
Fiber[@fiber_queue.label]= self
execute_block
end
private
def execute_block
@completed = false
@fiber_queue.async do
@result ||= @block.call(*@args)
unless self == Fiber[:root_fiber]
Fiber.delete_fiber(@fiber_queue.label)
@block = nil # when @block becomes nil the fiber is dead
end
@completed = true
@resume_sem.signal
@result
end
end
public
def resume *args
raise FiberError, 'dead fiber called' if @block.nil?
# raise FiberError, 'double resume' if @has_transferred == true
execute_block if @completed # root fiber never dies
if @args.nil?
@args = args
else
@result = args.size > 1 ? args : args.first
end
@yield_sem.signal
@resume_sem.wait unless @has_transferred == :complete
@result
end
# yield is called from inside the block passed to Fiber.new and executes within the @fiber_queue
def yield *args
@result = args.size > 1 ? args : args.first
@resume_sem.signal
@yield_sem.wait
@result
end
def transfer *args
fiber = Fiber.current # the fiber in whose context the transfer call was made. Should be given a yield call to suspend execution of its block
@transferred_from = fiber
if Fiber[:root_fiber] == fiber # check for transfer to root fiber
self.resume *args
else
resume = lambda do |previous_fiber|
@has_transferred = :complete if self == previous_fiber && !previous_fiber.nil?
self.resume *args
end
fiber.instance_eval do
@has_transferred = true
self.yield *resume.call(@transferred_from)
@resume_sem.wait and @has_transferred = nil if @has_transferred == :complete
end
end
end
def alive?
[email protected]?
end
def inspect
"#<#{self.class}:0x#{self.object_id.to_s(16)}>"
end
def self.current
Fiber[Dispatch::Queue.current.label] || Fiber[:root_fiber]
end
def self.yield *args
raise FiberError, "can't yield from root fiber" unless fiber = Fiber[Dispatch::Queue.current.label]
fiber.yield *args
end
# class methods to get, set and delete fiber references
# wrapped with a serial queue for safe multi-threaded use
def self.[] fiber_id
@@fibers_queue.sync { return @@__fibers__[fiber_id] }
end
def self.[]= fiber_id, fiber
@@fibers_queue.sync { @@__fibers__[fiber_id]= fiber }
end
def self.delete_fiber fiber_id
@@fibers_queue.sync { @@__fibers__.delete(fiber_id) }
end
@@__fibers__ = {} # create class hash to enable look-up of individual fibers when using 'Fiber.yield'
@@fibers_queue = Dispatch::Queue.new('fibers_queue') # create serial queue to access class hash
Fiber[:root_fiber]= Fiber.new { |*args| args.size > 1 ? args : args.first } # create root fiber. Default behaviour is to return arguments it receives
end
require 'minitest/autorun'
describe "try to create new fiber without block" do
it "should raise ArgumentError" do
proc { Fiber.new }.must_raise ArgumentError
end
end
## Attempting to run the following test crashes macruby ##
# describe "get root fiber" do
# it "should be fiber" do
# f = Fiber.current
# f.must_be_instance_of Fiber
# end
# end
describe "return default value from resumed fiber" do
it "should return final value from fiber" do
fiber = Fiber.new { 1; 2; 'return from fiber' }
fiber.resume.must_equal 'return from fiber'
end
end
describe "return single value from resume" do
it "should return arg to resume from fiber" do
fiber = Fiber.new {|x| 1; 2; x }
fiber.resume('fred').must_equal 'fred'
end
end
describe "Setting variable inside and outside of fiber" do
it "should set var and return last value in fiber block" do
fiber = Fiber.new do |x|
str = Fiber.yield(x)
'fiber return value'
end
str = fiber.resume('first resume arg').must_equal 'first resume arg'
fiber.resume('second resume arg').must_equal 'fiber return value'
str.must_equal 'second resume arg'
end
end
describe "fiber dies after returning final value" do
it "should die after second resume and raise error on third resume" do
fiber = Fiber.new { Fiber.yield('yield value'); 'fiber return value'}
fiber.resume.must_equal 'yield value'
fiber.resume.must_equal 'fiber return value'
fiber.alive?.must_equal false
proc { fiber.resume }.must_raise FiberError # dead fiber
end
end
describe "yield value from fiber" do
it "should return default value on first resume, nil on second, raise error on third" do
fiber = Fiber.new { Fiber.yield "yield from fiber"}
fiber.resume.must_equal "yield from fiber"
fiber.resume.must_equal nil
proc { fiber.resume }.must_raise FiberError # dead fiber
end
end
describe "yield value from fiber with arg" do
it "should return arg value on first and second resume, then raise error on third" do
fiber = Fiber.new { |x| Fiber.yield x }
fiber.resume('arg to first resume').must_equal 'arg to first resume'
fiber.resume('arg to second resume').must_equal 'arg to second resume'
proc { fiber.resume }.must_raise FiberError # dead fiber
end
end
describe "yield value in loop" do
it "should stay alive" do
fiber = Fiber.new do
start = 0
loop do
Fiber.yield start
start += 1
end
end
fiber.resume.must_equal 0
fiber.resume.must_equal 1
fiber.resume.must_equal 2
fiber.alive?.must_equal true
end
end
describe "yield value in loop - with resume arg" do
it "fiber should stay alive" do
fiber = Fiber.new do |start|
loop do
Fiber.yield start
start += 1
end
end
fiber.resume(10).must_equal 10
fiber.resume.must_equal 11
fiber.resume(57).must_equal 12 # because yield is called within a loop, the resume arg only applies on the first call
fiber.alive?.must_equal true
end
end
describe "Yield calculated value from resume arg" do
it "should return altered arg value from first resume, unaltered arg value on second resume and raise error on third" do
fiber = Fiber.new do |first|
second = Fiber.yield first + 2
end
fiber.resume(10).must_equal 12
fiber.resume(89).must_equal 89 # the second resume continues from where yield left off, therefore fiber returns arg to resume
proc {fiber.resume(73)}.must_raise FiberError # dead fiber
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment