为什么我们需要纤维

fl0*_*00r 95 ruby lambda closures fibers proc

对于Fibers,我们有一个典型的例子:生成Fibonacci数

fib = Fiber.new do  
  x, y = 0, 1 
  loop do  
    Fiber.yield y 
    x,y = y,x+y 
  end 
end
Run Code Online (Sandbox Code Playgroud)

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

def clsr
  x, y = 0, 1
  Proc.new do
    x, y = y, x + y
    x
  end
end
Run Code Online (Sandbox Code Playgroud)

所以

10.times { puts fib.resume }
Run Code Online (Sandbox Code Playgroud)

prc = clsr 
10.times { puts prc.call }
Run Code Online (Sandbox Code Playgroud)

将返回相同的结果.

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

Ale*_*x D 222

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

在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)>
Run Code Online (Sandbox Code Playgroud)

它们Enumerator是Enumerable对象,它们的each方法产生了原始迭代器方法产生的元素,如果用块调用的话.在我刚给出的例子中,返回的Enumerator reverse_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"
Run Code Online (Sandbox Code Playgroud)

您可能听说过"内部迭代器"和"外部迭代器"(两者的良好描述在"四人帮"设计模式一书中给出).上面的示例显示了枚举器可用于将内部迭代器转换为外部迭代器.

这是制作自己的枚举器的一种方法:

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
Run Code Online (Sandbox Code Playgroud)

我们来试试吧:

e = SomeClass.new.an_iterator
e.next  # => 1
e.next  # => 2
e.next  # => 3
Run Code Online (Sandbox Code Playgroud)

等一下......那里有什么奇怪的吗?您以直线代码编写yield语句an_iterator,但Enumerator可以一次运行一个.在调用之间next,执行an_iterator是"冻结".每次调用时next,它都会继续运行到以下yield语句,然后再次"冻结".

你能猜出这是如何实现的吗?Enumerator将呼叫包裹an_iterator在光纤中,并传递一个暂停光纤的块.所以每次an_iterator产生块时,它运行的光纤都会被挂起,并在主线程上继续执行.下次调用时next,它会将控制传递给光纤,块返回,并an_iterator从中断处继续.

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

这与光纤persay没有关系,但是让我再提一下你可以用枚举器做的事情:它们允许你将更高阶的Enumerable方法应用于除了之外的其他迭代器each.想想看:通常所有可枚举的方法,其中包括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]
Run Code Online (Sandbox Code Playgroud)

在没有块的情况下调用迭代器会返回一个Enumerator,然后您可以在其上调用其他Enumerable方法.

回到光纤,您是否使用了takeEnumerable中的方法?

class InfiniteSeries
  include Enumerable
  def each
    i = 0
    loop { yield(i += 1) }
  end
end
Run Code Online (Sandbox Code Playgroud)

如果有什么东西调用那个each方法,它看起来应该永远不会返回,对吧?看一下这个:

InfiniteSeries.new.take(10) # => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Run Code Online (Sandbox Code Playgroud)

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

您还可以使用纤维建立通用的协同设施.我从未在我的任何程序中使用过协程,但这是一个很好的概念.

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

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

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

我确信有很多理由说明为什么这样的事情可能不实用(至少目前为止),但我再次尝试向您展示一些可能性.谁知道; 一旦你掌握了这个概念,你就可以想出一些全新的应用程序,而这个应用程序还没有人想到过!

  • 这个答案是如此之好,以至于它应该被写成某个地方的博客文章. (13认同)
  • `take`不需要纤维.相反,`take`在第n次收益期间简单地断开.当在块内使用时,`break`将控制返回到定义块的帧.`a = []; InfiniteSeries.new.each {| x | 一个<< x; 如果a.length == 10}则中断; 了` (2认同)

Ali*_*kau 21

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

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
Run Code Online (Sandbox Code Playgroud)

打印这个:

some code
return
received param: param
etc
Run Code Online (Sandbox Code Playgroud)

使用其他ruby功能实现此逻辑将不太可读.

有了这个功能,良好的光纤使用是做手动协同调度(作为线程替换).Ilya Grigorik有一个很好的例子,说明如何将异步库(eventmachine在这种情况下)转换为看起来像同步API而不会失去异步执行的IO调度优势.这是链接.