对于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功能?
答案 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方法,包括map
,select
,include?
,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。