为什么我们需要纤维


100

对于纤维,我们有一个经典的例子:生成斐波那契数

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

为什么我们这里需要纤维?我可以使用相同的Proc重写它(实际上是关闭)

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写什么样的东西,而不能使用lambda和其他很酷的Ruby功能来写?


4
古老的斐波那契示例只是最可能的诱因;-)甚至可以使用公式来计算O(1)中的任何斐波那契数。
usr 2012年

17
问题不是关于算法,而是关于理解光纤:)
fl00r 2012年

Answers:


229

光纤是您可能永远不会直接在应用程序级代码中使用的东西。它们是流控制原语,可用于构建其他抽象,然后将其用于更高级别的代码中。

Ruby中纤维的第一用途可能是实现Enumerators,这是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是Enumerable对象,each如果使用块调用它们,则它们的方法将产生由原始迭代器方法产生的元素。在我刚刚给出的示例中,由返回的Enumerator reverse_each具有一个each产生3,2,1 的方法。由返回的Enumerator 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作为直线码,但枚举可以运行他们一次一个。在对的调用之间nextan_iterator“冻结” 的执行。每次调用时next,它将继续运行至以下yield语句,然后再次“冻结”。

你能猜出它是如何实现的吗?枚举器将调用包装到an_iterator光纤中,并传递一个使光纤挂起的块。因此,每次an_iterator屈服于该块时,其运行的光纤都将挂起,并在主线程上继续执行。下次调用时next,它将控制权传递给光纤,该块返回,并an_iterator从中断处继续。

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

这与纤维的使用无关,但是让我再提一提您可以使用Enumerators进行的另一件事:它们允许您将高阶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]

调用无块的迭代器将返回一个Enumerator,然后可以在其上调用其他Enumerable方法。

回到光纤,您是否使用了takeEnumerable中的方法?

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

您还可以使用纤维构建通用的协程设备。我从未在任何程序中使用过协程,但这是一个很好的概念。

我希望这会让您对这些可能性有所了解。正如我在一开始所说的那样,纤维是一种低级的流量控制原语。它们使您可以在程序中维护多个控制流“位置”(如书页中的不同“书签”)并根据需要在它们之间进行切换。由于任意代码都可以在光纤中运行,因此您可以调用光纤上的第三方代码,然后“冻结”它,并在其回调您控制的代码时继续执行其他操作。

想象一下这样的事情:您正在编写一个服务于许多客户端的服务器程序。与客户端的完整交互涉及到一系列步骤,但是每个连接都是短暂的,您必须记住连接之间每个客户端的状态。(听起来像Web编程吗?)

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

我敢肯定有很多原因使这种事情可能不可行(至少目前是这样),但是我只是想向您展示一些可能性。谁知道; 一旦有了概念,您可能会想出一些全新的应用程序,而其他人还没有想到!


谢谢您的答复!那么,为什么他们不chars使用闭包来实现或其他枚举器呢?
fl00r 2012年

@ fl00r,我正在考虑添加更多信息,但是我不知道此答案是否已经太长...您想要更多吗?
Alex D

13
这个答案是如此之好,以至于应该作为博客文章写在某处,methinks。
杰森·沃格勒

1
更新:看起来Enumerable在Ruby 2.0中将包含一些“惰性”方法。
Alex D

2
take不需要纤维。相反,take只需在第n个收益率期间中断即可。在块内使用时,break将控制返回到定义该块的框架。a = [] ; InfiniteSeries.new.each { |x| a << x ; break if a.length == 10 } ; a
马修

22

与具有定义的入口和出口点的闭包不同,光纤可以多次保存其状态并返回(屈服):

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调度的优势。这是链接


谢谢!我阅读了文档,所以我了解了光纤中许多入口和出口的所有魔术。但是我不确定这些东西会让生活更轻松。我认为尝试遵循所有这些简历和收益并不是一个好主意。似乎很难解开思路。因此,我想了解在某些情况下这种纤维提示是好的解决方案。Eventmachine很酷,但不是了解光纤的最佳场所,因为首先您应该了解所有这些反应堆模式。因此,我相信我可以physical meaning在更简单的示例中理解光纤
fl00r 2012年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.