102

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

4

2 回答 2

230

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

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)>

这些Enumerators 是 Enumerable 对象,它们的each方法产生的元素将由原始迭代器方法产生,如果它被一个块调用的话。在我刚刚给出的示例中,返回的 Enumeratorreverse_each有一个each产生 3,2,1 的方法。产生的枚举器返回chars“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

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

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

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

这与 Fiber Persay 无关,但让我再提一下您可以使用 Enumerators 做的另一件事:它们允许您将高阶 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 方法。

回到纤维,你用过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]

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

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

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

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

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

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

于 2012-02-08T13:17:08.473 回答
22

与具有定义的入口和出口点的闭包不同,纤程可以保持其状态并多次返回(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 调度的优势。这是链接

于 2012-01-29T12:39:48.770 回答