为什么我们需要纤维

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

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

为什么我们需要纤维? 我可以用相同的Proc重写这个(实际上是closures的)

 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 } 

将返回相同的结果。

那么纤维有什么优点? 什么样的东西,我可以用纤维写我不能用lambdas和其他很酷的Rubyfunction?

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

Ruby中纤维的第一次使用可能是实现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是Enumerable对象,并且它们的each方法都产生原始迭代器方法产生的元素,如果它被块调用。 在我刚刚给出的例子中,由reverse_each返回的Enumerator具有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" 

你可能听说过“内部迭代器”和“外部迭代器”(在“四人帮”devise模式书中给出了对这两者的很好的描述)。 上面的例子展示了枚举器可以用来把一个内部的迭代器变成一个外部的迭代器。

这是使自己的调查员的一种方法:

 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块时,正在运行的光纤被挂起,并且在主线程上继续执行。 下一次你下一次调用,它将控制权交给纤维, 块返回 ,并且an_iterator继续它停止的地方。

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

这并不需要用纤维来实现,但是让我们再来介绍一下使用枚举器可以做的更多事情:它们允许您将更高阶的Enumerable方法应用于其他迭代器以外的其他迭代器。 想想看:通常所有的Enumerable方法,包括mapselect ,都include?inject等等, 所有的工作都是each产生的。 但是如果一个对象有其他迭代器而不是each呢?

 irb(main):001:0> "Hello".chars.select { |c| c =~ /[AZ]/ } => ["H"] irb(main):002:0> "Hello".bytes.sort => [72, 101, 108, 108, 111] 

调用没有阻塞的迭代器会返回一个枚举器,然后你可以调用其他的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] 

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

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

我希望这给你一些可能性的想法。 正如我刚开始所说的那样,光纤是一个低级别的stream控制原语。 它们可以在程序中保持多个控制stream“位置”(如书中不同的“书签”),并根据需要在它们之间切换。 由于任意代码都可以在光纤中运行,因此可以在光纤上调用第三方代码,然后“冻结”该代码,并在调用代码时继续执行其他操作。

设想一下这样的事情:你正在写一个服务器程序,它将服务于许多客户。 与客户进行完整的交互涉及一系列步骤,但每个连接都是暂时的,并且必须记住连接之间每个客户的状态。 (听起来像networking编程?)

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

我相信这种事情可能不太现实(至less现在是这样)有很多原因,但我只是试图向你展示一些可能性。 谁知道; 一旦你得到了这个概念,你可能会想出一些全新的应用程序,这是别人没有想到的!

与具有确定的进入和退出点的闭合不同,纤维可以保持其状态并多次返回(屈服):

 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特性来实现这个逻辑的可读性会降低。

有了这个function,良好的光纤使用是做手动合作调度(如线程更换)。 Ilya Grigorik在如何将一个asynchronous库(在这个例子中是eventmachine )转换成看起来像一个同步API的优秀例子的同时,却没有失去asynchronous执行的IO调度的优点。 这是链接 。

Interesting Posts