Fiber 可能永远不会直接在应用程序级代码中使用。它们是一种流控制原语,您可以使用它来构建其他抽象,然后在更高级别的代码中使用它们。
可能在 Ruby 中使用纤维的 #1 是实现 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 方法产生的元素将由原始迭代器方法产生,如果它被一个块调用的话。在我刚刚给出的示例中,reverse_each 返回的 Enumerator 有一个 each 方法,它产生 3,2,1。 chars 返回的 Enumerator 产生 "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
等一下……那里有什么奇怪的地方吗?您将an_iterator 中的yield 语句编写为直线代码,但枚举器可以一次运行一个。在调用next 之间,an_iterator 的执行被“冻结”。每次调用next,它都会继续运行到下面的yield 语句,然后再次“冻结”。
你能猜出这是如何实现的吗?枚举器将对an_iterator 的调用包装在一个纤程中,并传递一个暂停纤程的块。因此,每次an_iterator 屈服于该块时,它正在运行的光纤被暂停,并且在主线程上继续执行。下次您调用 next 时,它会将控制权交给光纤,块返回,然后an_iterator 从中断处继续。
想想在没有纤维的情况下需要做些什么是很有启发性的。每个想要提供内部和外部迭代器的类都必须包含显式代码来跟踪对next 的调用之间的状态。每次调用 next 都必须检查该状态,并在返回值之前对其进行更新。使用纤程,我们可以自动将任何内部迭代器转换为外部迭代器。
这与纤维的说法无关,但让我再提一下您可以使用枚举器做的另一件事:它们允许您将高阶 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]
调用不带block的迭代器会返回一个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]
我不知道这是否在引擎盖下使用了纤维,但它可以。 Fibers 可用于实现无限列表和序列的惰性求值。对于使用 Enumerators 定义的一些惰性方法的示例,我在这里定义了一些:https://github.com/alexdowad/showcase/blob/master/ruby-core/collections.rb
您还可以使用纤程构建通用协程工具。我从来没有在我的任何程序中使用过协程,但这是一个很好的概念。
我希望这能让您对可能性有所了解。正如我一开始所说的,纤维是一种低级的流量控制原语。它们可以在您的程序中维护多个控制流“位置”(如书页中的不同“书签”)并根据需要在它们之间切换。由于任意代码可以在纤程中运行,因此您可以调用纤程上的第 3 方代码,然后“冻结”它并在它回调到您控制的代码时继续执行其他操作。
想象一下这样的事情:您正在编写一个服务于许多客户端的服务器程序。与客户端的完整交互需要经历一系列步骤,但每个连接都是瞬态的,您必须记住连接之间每个客户端的状态。 (听起来像网络编程?)
您可以为每个客户端维护一个纤程,而不是显式存储该状态,并在每次客户端连接时检查它(以查看他们必须执行的下一个“步骤”是什么)。识别客户端后,您将检索他们的光纤并重新启动它。然后在每次连接结束时,您将暂停光纤并再次存储它。这样,您可以编写直线代码来实现完整交互的所有逻辑,包括所有步骤(就像您的程序在本地运行时自然会做的那样)。
我确信这样的事情可能不实用的原因有很多(至少目前如此),但我只是想再次向您展示一些可能性。谁知道;一旦你掌握了这个概念,你可能会想出一些其他人还没有想到的全新应用程序!