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

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

为什么我们需要 Fibers?我可以用同样的 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 特性?

最佳答案

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

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 是 Enumerable 对象,它们的 each 方法产生的元素本来可以由原始迭代器方法产生,如果它被块调用。在我刚刚给出的例子中,由 reverse_each 返回的 Enumerator 有一个 each 方法,它产生 3,2,1。 chars 返回的 Enumerator 产生“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 语句编写为直线代码,但 Enumerator 可以一次运行它们。在对 next 的调用之间, an_iterator 的执行被“卡住”。每次调用 next 时,它都会继续运行到下面的 yield 语句,然后再次“卡住”。

你能猜出这是如何实现的吗? Enumerator 将对 an_iterator 的调用封装在纤程中,并传递一个挂起纤程的块。因此,每次 an_iterator 向块屈服时,它正在运行的光纤被挂起,并在主线程上继续执行。下次您调用 next 时,它会将控制权传递给光纤,块返回,并且 an_iterator 从它停止的地方继续。

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

这与 Fibers persay 无关,但让我提一下您可以使用 Enumerator 做的另一件事:它们允许您将高阶 Enumerable 方法应用于除 each 之外的其他迭代器。想一想:通常所有的 Enumerable 方法,包括 mapselectinclude?inject 、 123 14 14 14 141 14 141 14 141个所有元素上的所有工作,等等。但是如果一个对象有除 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 中的 each 方法?
class InfiniteSeries
  include Enumerable
  def each
    i = 0
    loop { yield(i += 1) }
  end
end

如果有任何东西调用了 take 方法,它看起来应该永远不会返回,对吧?看一下这个:
InfiniteSeries.new.take(10) # => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

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

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

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

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

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

我相信有很多原因为什么这样的事情可能不切实际(至少现在是这样),但我再次只是想向您展示一些可能性。谁知道;一旦你有了这个概念,你可能会想出一些其他人没有想到的全新应用程序!

关于ruby - 为什么我们需要纤维,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/9052621/

10-13 09:29