2012-01-29 118 views
80

對於纖維我們已經得到了經典的例子:斐波那契數的生成我們爲什麼需要纖維

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

爲什麼我們在這裏需要纖維?我可以用一樣的PROC改寫這個包(closure,其實)

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特性做什麼?

+4

舊的斐波納契例子只是最差的可能動機;-)甚至有一個公式可以用來計算O(1)中的_any_斐波那契數。 – usr 2012-01-29 12:30:03

+15

問題不在於算法,而在於理解纖維:) – fl00r 2012-01-29 18:20:12

回答

197

纖維是您可能永遠不會直接在應用程序級代碼中使用的東西。它們是一個流控制原語,您可以使用它來構建其他抽象,然後在較高級別的代碼中使用這些抽象。

可能是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 s爲可枚舉的目的,以及它們的each方法得到這將已經產生由原始迭代方法中的元素,如果它被稱爲一個塊。在我剛剛給出的例子中,由reverse_each返回的枚舉器具有產生3,2,1的each方法。枚舉器返回chars產生「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作爲直線代碼,但枚舉器可以一次運行它們一個。在對next的調用之間,執行an_iterator被「凍結」。每次您撥打next時,它會繼續運行至以下yield聲明,然後再次「凍結」。

你能猜到這是如何實現的嗎?枚舉器在光纖中將呼叫包裝爲an_iterator,並且傳遞一個塊,其中暫停光纖。因此,每當an_iterator產生該塊時,它所運行的光纖被暫停,並且主線程上的執行繼續。下一次呼叫next時,它將控制權交給光纖,該塊返回,並且an_iterator繼續從光纖停止。

想想沒有光纖的情況下做什麼需要什麼。每個想要提供內部和外部迭代器的類都必須包含顯式代碼,以跟蹤next調用之間的狀態。接下來的每次調用都必須檢查該狀態,並在返回值之前更新它。使用光纖,我們可以自動將任何內部迭代器轉換爲外部迭代器。

這不是關於纖維的問題,但讓我再提一點你可以用統計員做的事情:他們允許你將更高階的Enumerable方法應用到除each以外的其他迭代器。想想看:通常所有可枚舉的方法,包括mapselectinclude?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] 

調用沒有塊的迭代器會返回一個枚舉器,然後您可以調用其他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

您也可以使用光纖構建通用協同工具。我從未在我的任何程序中使用過協程,但這是一個很好的概念。

我希望這給你一些可能性的想法。正如我剛開始所說的,光纖是一種低級別的流控制原語。它們可以在程序中保持多個控制流「位置」(如書中不同的「書籤」),並根據需要在它們之間切換。由於任意代碼都可以在光纖中運行,因此您可以在光纖上調用第三方代碼,然後「凍結」該代碼並在回調到您控制的代碼時繼續執行其他操作。

想象一下這樣的事情:您正在編寫一個服務器程序,它將爲許多客戶端提供服務。與客戶端的完整交互涉及經歷一系列步驟,但每個連接都是暫時的,並且您必須記住每個客戶端在連接之間的狀態。 (聽起來像網絡編程?)

而不是顯式存儲該狀態,並檢查每次客戶端連接(看看他們要做的下一個「步驟」),您可以爲每個客戶端維護光纖。識別客戶端後,您將檢索其光纖並重新啓動它。然後在每次連接結束時,您將暫停光纖並再次存儲。這樣,您可以編寫直線代碼來實現完整交互的所有邏輯,包括所有步驟(就像您的程序在本地運行時一樣)。

我確定有很多原因可能導致這樣的事情不可行(至少現在),但我只是想告訴你一些可能性。誰知道;一旦你得到了這個概念,你可能會想出一些全新的應用程序,這是別人沒有想到的!

+0

謝謝你的回答!那麼,爲什麼他們不實施'chars'或其他只是關閉的枚舉? – fl00r 2012-02-08 13:23:49

+0

@ fl00r,我想添加更多的信息,但我不知道這個答案是否已經太長了......你想要更多嗎? – 2012-02-08 18:04:44

+0

我想要! :)非常高興! – fl00r 2012-02-08 21:41:14

17

不像倒閉,其中有一個定義的進入和退出點,纖維可以保持自己的狀態和回報(收益率)多次:

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 

的這個實施其他紅寶石功能的邏輯可讀性會降低。

有了這個功能,良好的光纖使用就是做手動合作調度(如線程替換)。 Ilya Grigorik在如何將異步庫(在這種情況下爲eventmachine)轉換爲看起來像同步API的優秀示例,卻沒有失去異步執行的IO調度優勢。這裏是link

+0

謝謝!我閱讀文檔,所以我理解了所有這些帶有許多條目並退出光纖的魔術。但我不確定這些東西讓生活更輕鬆。我不認爲嘗試關注所有這些簡歷和收益率是個好主意。它看起來像一個難以解開的提示。所以我想了解是否有這種情況下纖維的提示是很好的解決方案。 Eventmachine很酷,但不是理解纖維的最佳場所,因爲首先你應該瞭解所有這些反應堆模式的事情。所以我相信我可以在更簡單的例子中理解纖維的「物理意義」 – fl00r 2012-01-29 17:48:09