【问题标题】:Why do we need fibers为什么我们需要纤维
【发布时间】:2012-02-21 13:59:35
【问题描述】:

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

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 功能写的东西?

【问题讨论】:

  • 旧的斐波那契示例只是最糟糕的动力;-) 甚至还有一个公式可以用来计算 O(1) 中的任何斐波那契数。
  • 问题不在于算法,而在于理解纤维 :)

标签: ruby lambda closures fibers proc


【解决方案1】:

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

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

【讨论】:

  • 谢谢!我阅读了文档,所以我理解了所有这些在光纤内部有许多入口和出口的魔法。但我不确定这些东西会让生活更轻松。我认为尝试遵循所有这些简历和收益并不是一个好主意。它看起来像一个难以解开的线索。所以我想了解是否存在这种纤维束是很好的解决方案的情况。 Eventmachine 很酷,但不是了解纤维的最佳场所,因为首先您应该了解所有这些反应器模式的东西。所以我相信我可以在更简单的例子中理解纤维physical meaning
【解决方案2】:

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

可能在 Ruby 中使用纤维的 #1 是实现 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)>

这些Enumerators 是Enumerable 对象,它们的each 方法产生的元素将由原始迭代器方法产生,如果它被一个块调用的话。在我刚刚给出的示例中,reverse_each 返回的 Enumerator 有一个 each 方法,它产生 3,2,1。 chars 返回的 Enumerator 产生 "c","b","a" (等等)。但是,与原来的迭代器方法不同的是,如果你在它上面反复调用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"

您可能听说过“内部迭代器”和“外部迭代器”(“Gang of Four”设计模式一书中对两者都有很好的描述)。上面的例子展示了 Enumerators 可以用来将一个内部迭代器变成一个外部迭代器。

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

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

等一下……那里有什么奇怪的地方吗?您将an_iterator 中的yield 语句编写为直线代码,但枚举器可以一次运行一个。在调用next 之间,an_iterator 的执行被“冻结”。每次调用next,它都会继续运行到下面的yield 语句,然后再次“冻结”。

你能猜出这是如何实现的吗?枚举器将对an_iterator 的调用包装在一个纤程中,并传递一个暂停纤程的块。因此,每次an_iterator 屈服于该块时,它正在运行的光纤被暂停,并且在主线程上继续执行。下次您调用 next 时,它会将控制权交给光纤,块返回,然后an_iterator 从中断处继续。

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

这与纤维的说法无关,但让我再提一下您可以使用枚举器做的另一件事:它们允许您将高阶 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]

调用不带block的迭代器会返回一个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]

我不知道这是否在引擎盖下使用了纤维,但它可以。 Fibers 可用于实现无限列表和序列的惰性求值。对于使用 Enumerators 定义的一些惰性方法的示例,我在这里定义了一些:https://github.com/alexdowad/showcase/blob/master/ruby-core/collections.rb

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

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

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

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

我确信这样的事情可能不实用的原因有很多(至少目前如此),但我只是想再次向您展示一些可能性。谁知道;一旦你掌握了这个概念,你可能会想出一些其他人还没有想到的全新应用程序!

【讨论】:

  • 感谢您的回答!那么为什么他们不实现 chars 或其他仅使用闭包的枚举器呢?
  • @fl00r,我正在考虑添加更多信息,但我不知道这个答案是否已经太长了......你想要更多吗?
  • 这个答案太好了,我觉得应该写成博客文章。
  • 更新:看起来Enumerable 将在 Ruby 2.0 中包含一些“惰性”方法。
  • take 不需要光纤。相反,take 只是在第 n 个产量期间中断。在块内使用时,break 将控制权返回给定义块的框架。 a = [] ; InfiniteSeries.new.each { |x| a &lt;&lt; x ; break if a.length == 10 } ; a
猜你喜欢
  • 2019-06-09
  • 2014-06-18
  • 2017-02-26
  • 2011-04-03
  • 2017-07-27
  • 2020-09-21
  • 2020-03-09
  • 2018-12-24
  • 2012-04-08
相关资源
最近更新 更多