对于纤维,我们有经典的例子:斐波那契数的生成
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 方法,包括 map
、 select
、 include?
、 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/