Created
January 5, 2012 14:05
-
-
Save alskipp/1565393 to your computer and use it in GitHub Desktop.
Macruby Fibers using GCD (transfer method implemented, 'double resume' error needs implementing)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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