为什么我们需要纤维

时间:2012-01-29 10:55:00

标签: ruby lambda closures fibers proc

对于Fibers,我们有一个经典的例子:产生Fibonacci数

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写什么样的东西我不能用lambdas和其他很酷的Ruby功能?

2 个答案:

答案 0 :(得分:218)

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

Ruby中#1光纤的使用可能是实现Enumerator,它是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是可枚举对象,它们的each方法产生了原始迭代器方法产生的元素,如果用块调用的话。在我刚给出的示例中,reverse_each返回的枚举器有一个each方法,产生3,2,1。由chars返回的枚举器产生&#34; c&#34;,&#34; b&#34;,&#34; a&#34; (等等)。但是,与原始迭代器方法不同,如果您反复调用next,Enumerator也可以逐个返回元素:

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"

您可能听说过&#34;内部迭代器&#34;和#34;外部迭代器&#34; (两者的良好描述在&#34; Gang of Four&#34; Design Patterns一书中给出)。上面的示例显示了枚举器可用于将内部迭代器转换为外部迭代器。

这是制作自己的枚举器的一种方法:

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的执行被冻结&#34;。每次拨打next时,它都会继续向下运行到yield语句,然后&#34;冻结&#34;试。

你能猜出这是如何实现的吗? Enumerator将对an_iterator的调用包装在光纤中,并传递挂起光纤的块。因此每次an_iterator产生块时,它运行的光纤都会被挂起,并在主线程上继续执行。下次拨打next时,它会将控制传递给光纤,块返回an_iterator会从中断处继续。

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

这与光纤persay无关,但是让我再提一下你可以用枚举器做的事情:它们允许你将更高阶的Enumerable方法应用于each以外的其他迭代器。考虑一下:通常所有的Enumerable方法,包括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方法。

回到光纤,您是否使用了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

您还可以使用纤维建立通用的协同设施。我还没有在我的任何程序中使用过协程,但这是一个很好的概念。

我希望这能让你对这些可能性有所了解。正如我在开始时所说,光纤是一种低级流控制原语。它们可以保持多个控制流量位置&#34;在你的程序中(如书中的不同&#34;书签和#34;并根据需要在它们之间切换)。由于任意代码可以在光纤中运行,因此您可以在光纤上调用第三方代码,然后&#34;冻结&#34;当它回调你控制的代码时,它会继续做其他事情。

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

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

我确定有很多理由说明为什么这样的事情可能不实用(至少目前为止),但我再次尝试向您展示一些可能性。谁知道;一旦你掌握了这个概念,你就可以想出一些全新的应用程序,而这个应用程序还没有人想到过!

答案 1 :(得分:20)

与具有已定义的进入和退出点的闭包不同,纤维可以保留其状态并多次返回(产量):

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

使用其他ruby功能实现此逻辑将不太可读。

使用此功能,良好的光纤使用是做手动协同调度(作为线程替换)。 Ilya Grigorik有一个很好的例子,说明如何将异步库(在这种情况下为eventmachine)变成看起来像同步API而不会失去异步执行的IO调度优势。这是link