2012-01-29 237 views
80

对于纤维我们已经得到了经典的例子:斐波那契数的生成我们为什么需要纤维

fib = Fiber.new do 
    x, y = 0, 1 
    loop do 
    Fiber.yield y 
    x,y = y,x+y 
    end 
end 

为什么我们在这里需要纤维?我可以用一样的PROC改写这个包(closure,其实)

def clsr 
    x, y = 0, 1 
    Proc.new do 
    x, y = y, x + y 
    x 
    end 
end 

所以

10.times { puts fib.resume } 

prc = clsr 
10.times { puts prc.call } 

将仅返回相同的结果。

那么纤维有什么优点。我可以用Fibers写什么样的东西,我不能用lambdas和其他很酷的Ruby特性做什么?

+4

旧的斐波纳契例子只是最差的可能动机;-)甚至有一个公式可以用来计算O(1)中的_any_斐波那契数。 – usr 2012-01-29 12:30:03

+15

问题不在于算法,而在于理解纤维:) – fl00r 2012-01-29 18:20:12

回答

197

纤维是您可能永远不会直接在应用程序级代码中使用的东西。它们是一个流控制原语,您可以使用它来构建其他抽象,然后在较高级别的代码中使用这些抽象。

可能是Ruby中纤维的第一次使用是实现Enumerator s,这是Ruby 1.9中的一个核心Ruby类。这些都是令人难以置信的有用。

在Ruby 1.9中,如果您在核心类上调用几乎任何迭代器方法,则不通过传递一个块,它将返回Enumerator

irb(main):001:0> [1,2,3].reverse_each 
=> #<Enumerator: [1, 2, 3]:reverse_each> 
irb(main):002:0> "abc".chars 
=> #<Enumerator: "abc":chars> 
irb(main):003:0> 1.upto(10) 
=> #<Enumerator: 1:upto(10)> 

这些Enumerator s为可枚举的目的,以及它们的each方法得到这将已经产生由原始迭代方法中的元素,如果它被称为一个块。在我刚刚给出的例子中,由reverse_each返回的枚举器具有产生3,2,1的each方法。枚举器返回chars产生“c”,“b”,“a”(依此类推)。但是,与原迭代法,枚举也可以通过一个返回的元素之一,如果你反复在调用它next

irb(main):001:0> e = "abc".chars 
=> #<Enumerator: "abc":chars> 
irb(main):002:0> e.next 
=> "a" 
irb(main):003:0> e.next 
=> "b" 
irb(main):004:0> e.next 
=> "c" 

您可能听说过“内部迭代器”和“外部迭代器”的(良好的两者的描述在“四人帮”设计模式书中给出)。上面的例子展示了枚举器可以用来把一个内部迭代器变成一个外部迭代器。

这是为了使自己的统计员一个办法:

class SomeClass 
    def an_iterator 
    # note the 'return enum_for...' pattern; it's very useful 
    # enum_for is an Object method 
    # so even for iterators which don't return an Enumerator when called 
    # with no block, you can easily get one by calling 'enum_for' 
    return enum_for(:an_iterator) if not block_given? 
    yield 1 
    yield 2 
    yield 3 
    end 
end 

让我们试一下:

e = SomeClass.new.an_iterator 
e.next # => 1 
e.next # => 2 
e.next # => 3 

等一下...没有任何东西看起来很奇怪吗?您将yield语句编写为an_iterator作为直线代码,但枚举器可以一次运行它们一个。在对next的调用之间,执行an_iterator被“冻结”。每次您拨打next时,它会继续运行至以下yield声明,然后再次“冻结”。

你能猜到这是如何实现的吗?枚举器在光纤中将呼叫包装为an_iterator,并且传递一个块,其中暂停光纤。因此,每当an_iterator产生该块时,它所运行的光纤被暂停,并且主线程上的执行继续。下一次呼叫next时,它将控制权交给光纤,该块返回,并且an_iterator继续从光纤停止。

想想没有光纤的情况下做什么需要什么。每个想要提供内部和外部迭代器的类都必须包含显式代码,以跟踪next调用之间的状态。接下来的每次调用都必须检查该状态,并在返回值之前更新它。使用光纤,我们可以自动将任何内部迭代器转换为外部迭代器。

这不是关于纤维的问题,但让我再提一点你可以用统计员做的事情:他们允许你将更高阶的Enumerable方法应用到除each以外的其他迭代器。想想看:通常所有可枚举的方法,包括mapselectinclude?inject,等等,each产生的元素都工作。但是如果一个对象除了each之外还有其他迭代器呢?

irb(main):001:0> "Hello".chars.select { |c| c =~ /[A-Z]/ } 
=> ["H"] 
irb(main):002:0> "Hello".bytes.sort 
=> [72, 101, 108, 108, 111] 

调用没有块的迭代器会返回一个枚举器,然后您可以调用其他Enumerable方法。

回到纤维,你有没有使用Enumerable中的take方法?

class InfiniteSeries 
    include Enumerable 
    def each 
    i = 0 
    loop { yield(i += 1) } 
    end 
end 

如果有任何调用each方法,它看起来像它应该不会再回来了吧?看看这个:

InfiniteSeries.new.take(10) # => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 

我不知道这是否使用纤维罩下,但它可以。纤维可用于实现无限列表和一系列懒惰评估。有关枚举器定义的一些惰性方法的示例,我在此定义了一些:https://github.com/alexdowad/showcase/blob/master/ruby-core/collections.rb

您也可以使用光纤构建通用协同工具。我从未在我的任何程序中使用过协程,但这是一个很好的概念。

我希望这给你一些可能性的想法。正如我刚开始所说的,光纤是一种低级别的流控制原语。它们可以在程序中保持多个控制流“位置”(如书中不同的“书签”),并根据需要在它们之间切换。由于任意代码都可以在光纤中运行,因此您可以在光纤上调用第三方代码,然后“冻结”该代码并在回调到您控制的代码时继续执行其他操作。

想象一下这样的事情:您正在编写一个服务器程序,它将为许多客户端提供服务。与客户端的完整交互涉及经历一系列步骤,但每个连接都是暂时的,并且您必须记住每个客户端在连接之间的状态。 (听起来像网络编程?)

而不是显式存储该状态,并检查每次客户端连接(看看他们要做的下一个“步骤”),您可以为每个客户端维护光纤。识别客户端后,您将检索其光纤并重新启动它。然后在每次连接结束时,您将暂停光纤并再次存储。这样,您可以编写直线代码来实现完整交互的所有逻辑,包括所有步骤(就像您的程序在本地运行时一样)。

我确定有很多原因可能导致这样的事情不可行(至少现在),但我只是想告诉你一些可能性。谁知道;一旦你得到了这个概念,你可能会想出一些全新的应用程序,这是别人没有想到的!

+0

谢谢你的回答!那么,为什么他们不实施'chars'或其他只是关闭的枚举? – fl00r 2012-02-08 13:23:49

+0

@ fl00r,我想添加更多的信息,但我不知道这个答案是否已经太长了......你想要更多吗? – 2012-02-08 18:04:44

+0

我想要! :)非常高兴! – fl00r 2012-02-08 21:41:14

17

不像倒闭,其中有一个定义的进入和退出点,纤维可以保持自己的状态和回报(收益率)多次:

f = Fiber.new do 
    puts 'some code' 
    param = Fiber.yield 'return' # sent parameter, received parameter 
    puts "received param: #{param}" 
    Fiber.yield #nothing sent, nothing received 
    puts 'etc' 
end 

puts f.resume 
f.resume 'param' 
f.resume 

打印此:

some code 
return 
received param: param 
etc 

的这个实施其他红宝石功能的逻辑可读性会降低。

有了这个功能,良好的光纤使用就是做手动合作调度(如线程替换)。 Ilya Grigorik在如何将异步库(在这种情况下为eventmachine)转换为看起来像同步API的优秀示例,却没有失去异步执行的IO调度优势。这里是link

+0

谢谢!我阅读文档,所以我理解了所有这些带有许多条目并退出光纤的魔术。但我不确定这些东西让生活更轻松。我不认为尝试关注所有这些简历和收益率是个好主意。它看起来像一个难以解开的提示。所以我想了解是否有这种情况下纤维的提示是很好的解决方案。 Eventmachine很酷,但不是理解纤维的最佳场所,因为首先你应该了解所有这些反应堆模式的事情。所以我相信我可以在更简单的例子中理解纤维的“物理意义” – fl00r 2012-01-29 17:48:09