2012-01-29 16 views
80

fibre abbiamo avuto esempio classico: la generazione di numeri di FibonacciPerché abbiamo bisogno fibre

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

Perché abbiamo bisogno Fibre qui? Posso riscrivere questo con lo stesso Proc (chiusura, in realtà)

def clsr 
    x, y = 0, 1 
    Proc.new do 
    x, y = y, x + y 
    x 
    end 
end 

Così

10.times { puts fib.resume } 

e

prc = clsr 
10.times { puts prc.call } 

tornerà lo stesso risultato.

Quindi quali sono i vantaggi delle fibre. Che tipo di cose che posso scrivere con Fibers non posso fare con lambdas e altre fantastiche funzioni di Ruby?

+4

Il vecchio esempio di fibonacci è solo il peggior motivatore possibile ;-) Esiste anche una formula che è possibile utilizzare per calcolare il numero _any_ fibonacci in O (1). – usr

+15

Il problema non riguarda l'algoritmo, ma la comprensione delle fibre :) – fl00r

risposta

197

Le fibre sono qualcosa che probabilmente non userete mai direttamente nel codice a livello di applicazione. Sono una primitiva di controllo del flusso che puoi usare per costruire altre astrazioni, che poi usi nel codice di livello superiore.

Probabilmente l'uso # 1 delle fibre in Ruby è quello di implementare Enumerator s, che sono una classe core di Ruby in Ruby 1.9. Questi sono incredibilmente utili.

In Ruby 1.9, se si chiama quasi qualsiasi metodo iteratore sulle classi principali, senza passare un blocco, esso restituisce un 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)> 

Questi Enumerator s sono oggetti enumerabili, e loro each metodi producono gli elementi che sono stati generati dal metodo iteratore originale, se fosse stato chiamato con un blocco. Nell'esempio che ho appena dato, l'Enumeratore restituito da reverse_each ha un metodo each che restituisce 3,2,1. L'enumeratore restituito da chars restituisce "c", "b", "a" (e così via). Ma, a differenza del metodo iteratore originale, l'Enumerator può anche restituire gli elementi uno ad uno se si chiama next su di esso più volte:

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" 

Potreste aver sentito parlare di "iteratori interne" e "iteratori esterni" (un buon la descrizione di entrambi è riportata nel libro "Pattern of Design" di Gang of Four). L'esempio sopra mostra che gli Enumeratori possono essere usati per trasformare un iteratore interno in uno esterno.

Questo è un modo per rendere i propri enumeratori:

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 

Proviamo:

e = SomeClass.new.an_iterator 
e.next # => 1 
e.next # => 2 
e.next # => 3 

Aspetta un minuto ... fa niente sembra strano lì? Hai scritto le dichiarazioni yield nel codice an_iterator come codice lineare, ma l'Enumeratore può eseguirle uno alla volta. Tra le chiamate a next, l'esecuzione di an_iterator è "congelata". Ogni volta che si chiama next, continua a scorrere fino alla seguente istruzione yield, quindi "si blocca" nuovamente.

Riesci a indovinare come viene implementato? L'Enumeratore esegue il wrapping della chiamata a an_iterator in una fibra e passa un blocco che sospende la fibra. Quindi ogni volta che an_iterator si arrende al blocco, la fibra su cui è in esecuzione viene sospesa e l'esecuzione continua sul thread principale. La prossima volta che chiami next, passa il controllo alla fibra, il blocco restituisce e an_iterator continua da dove era stato interrotto.

Sarebbe istruttivo pensare a cosa sarebbe necessario fare senza fibre. OGNI classe che desiderava fornire iteratori interni ed esterni dovrebbe contenere codice esplicito per tenere traccia dello stato tra le chiamate a next. Ogni chiamata al prossimo dovrebbe verificare quello stato e aggiornarlo prima di restituire un valore. Con le fibre, possiamo automaticamente convertire qualsiasi iteratore interno a uno esterno.

Questo non ha a che fare con la persistenza delle fibre, ma vorrei menzionare un'altra cosa che puoi fare con gli Enumeratori: ti permettono di applicare metodi Enumerabili di ordine superiore ad altri iteratori diversi da each. Pensateci: normalmente tutti i metodi enumerabili, tra cui map, select, include?, inject, e così via, tutto lavoro sugli elementi prodotti dai each. Ma cosa succede se un oggetto ha altri iteratori diversi da 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] 

Chiamando l'iteratore senza alcun blocco restituito un enumeratore, e quindi è possibile chiamare altri metodi enumerabili su questo.

Tornando alle fibre, è stato utilizzato il metodo take da Enumerable?

class InfiniteSeries 
    include Enumerable 
    def each 
    i = 0 
    loop { yield(i += 1) } 
    end 
end 

Semmai chiama che each metodo, sembra che non dovrebbe mai tornare, giusto? Verificare:

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

Non so se questo utilizza le fibre sotto il cofano, ma potrebbe. Le fibre possono essere utilizzate per implementare liste infinite e una valutazione lenta di una serie. Per un esempio di alcuni metodi lazy definiti con Enumeratori, ne ho definiti alcuni qui: https://github.com/alexdowad/showcase/blob/master/ruby-core/collections.rb

È inoltre possibile creare un impianto di coroutine generico utilizzando le fibre. Non ho mai usato le coroutine in nessuno dei miei programmi, ma è un buon concetto da sapere.

Spero che questo ti dia un'idea delle possibilità. Come ho detto all'inizio, le fibre sono una primitiva di controllo del flusso di basso livello. Consentono di mantenere più "posizioni" del flusso di controllo all'interno del programma (come diversi "segnalibri" nelle pagine di un libro) e di alternare tra loro come desiderato. Poiché il codice arbitrario può essere eseguito in una fibra, puoi chiamare il codice di terze parti su una fibra, quindi "congelarlo" e continuare a fare qualcos'altro quando richiama il codice che controlli.

Immagina qualcosa del genere: stai scrivendo un programma server che servirà molti clienti. Un'interazione completa con un cliente comporta una serie di passaggi, ma ogni connessione è transitoria e devi ricordare lo stato per ciascun client tra le connessioni. (Sembra programmazione web?)

Anziché memorizzare in modo esplicito tale stato e verificarlo ogni volta che un client si connette (per vedere quale passo successivo devono fare), è possibile mantenere una fibra per ogni client . Dopo aver identificato il cliente, recupereresti la fibra e la riavvii. Quindi alla fine di ogni connessione, si sospenderebbe la fibra e la si memorizzasse di nuovo. In questo modo, è possibile scrivere codice di linea per implementare tutta la logica per un'interazione completa, compresi tutti i passaggi (proprio come faresti naturalmente se il tuo programma fosse stato creato per essere eseguito localmente).

Sono sicuro che ci sono molte ragioni per cui una cosa del genere potrebbe non essere pratica (almeno per ora), ma di nuovo sto solo cercando di mostrarvi alcune delle possibilità. Chissà; una volta ottenuto il concetto, potresti trovare un'applicazione totalmente nuova che nessun altro ha ancora pensato!

+0

Grazie per la risposta! Quindi, perché non implementano 'chars' o altri enumeratori con solo chiusure? – fl00r

+0

@ fl00r, sto pensando di aggiungere ancora più informazioni, ma non so se questa risposta è già troppo lunga ... ne vuoi ancora? –

+0

Voglio! :) Con grande piacere! – fl00r

17

A differenza di chiusura, che hanno un punto di entrata e di uscita definita, fibre possono mantenere lo stato e ritorno (resa) molte volte:

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 

stampe questo:

some code 
return 
received param: param 
etc 

attuazione della presente la logica con altre funzionalità di rubino sarà meno leggibile.

Con questa funzione, l'utilizzo di fibre buone è di fare la programmazione cooperativa manuale (come sostituzione di thread). Ilya Grigorik ha un buon esempio su come trasformare una libreria asincrona (eventmachine in questo caso) in ciò che sembra un'API sincrona senza perdere i vantaggi della schedulazione IO dell'esecuzione asincrona. Ecco lo link.

+0

Grazie! Leggo i documenti, quindi comprendo tutta questa magia con molte voci e uscite all'interno della fibra. Ma non sono sicuro che queste cose rendano la vita più facile. Non penso che sia una buona idea cercare di seguire tutti questi curricula e rendimenti. Sembra una scotta difficile da districare. Quindi voglio capire se ci sono casi in cui questa scia di fibre è una buona soluzione. Eventmachine è cool, ma non è il posto migliore per capire le fibre, perché prima dovresti capire tutte le cose del modello di questo reattore. Quindi credo di poter capire il significato fisico delle fibre in un esempio più semplice – fl00r

Problemi correlati