Perché abbiamo bisogno di fibre


100

Per le fibre abbiamo un esempio classico: generazione di numeri di Fibonacci

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

Perché abbiamo bisogno di fibre qui? Posso riscriverlo 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 }

restituirà lo stesso risultato.

Allora quali sono i vantaggi delle fibre. Che tipo di cose posso scrivere con Fibers che non posso fare con lambda e altre fantastiche funzionalità di Ruby?


4
Il vecchio esempio di Fibonacci è solo il peggior motivatore possibile ;-) C'è anche una formula che puoi usare per calcolare qualsiasi numero di Fibonacci in O (1).
usr

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

Risposte:


229

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 n. 1 delle fibre in Ruby è quello di implementare Enumerators, che sono una classe principale di Ruby in Ruby 1.9. Questi sono incredibilmente utili.

In Ruby 1.9, se chiami quasi tutti i metodi iteratori sulle classi principali, senza passare un blocco, restituirà un file 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 Enumeratorsono oggetti enumerabili e i loro eachmetodi producono gli elementi che sarebbero stati prodotti dal metodo iteratore originale, se fosse stato chiamato con un blocco. Nell'esempio che ho appena fornito, l'Enumeratore restituito da reverse_eachha un eachmetodo che restituisce 3,2,1. L'Enumeratore restituito charsrestituisce "c", "b", "a" (e così via). MA, a differenza del metodo iteratore originale, l'Enumeratore può anche restituire gli elementi uno per uno se lo chiami nextripetutamente:

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 interni" e "iteratori esterni" (una buona descrizione di entrambi è fornita nel libro "Gang of Four" Design Patterns). L'esempio sopra mostra che gli enumeratori possono essere utilizzati per trasformare un iteratore interno in uno esterno.

Questo è un modo per creare i tuoi 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

Proviamolo:

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

Aspetta un attimo ... c'è qualcosa di strano lì? Hai scritto le yieldistruzioni an_iteratorcome codice lineare, ma l'Enumerator può eseguirle una alla volta . Tra le chiamate a next, l'esecuzione di an_iteratorè "congelata". Ogni volta che si chiama next, continua a scorrere fino yieldall'istruzione seguente , quindi "si blocca" di nuovo.

Riuscite a indovinare come viene implementato? L'Enumeratore avvolge la chiamata a an_iteratorin una fibra e passa un blocco che sospende la fibra . Quindi ogni volta che an_iteratorcede al blocco, la fibra su cui sta scorrendo viene sospesa e l'esecuzione continua sul thread principale. La prossima volta che si chiama next, passa il controllo alla fibra, il blocco ritorna e an_iteratorcontinua da dove era stato interrotto.

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

Questo non ha a che fare con le fibre persay, ma lasciatemi 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 il lavoro sugli elementi derivanti 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]

La chiamata dell'iteratore senza blocco restituisce un enumeratore, quindi puoi chiamare altri metodi Enumerable su questo.

Tornando alle fibre, hai usato il takemetodo di Enumerable?

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

Se qualcosa chiama quel eachmetodo, sembra che non dovrebbe mai tornare, giusto? Controllalo:

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

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

Puoi anche costruire una struttura coroutine per uso generale usando le fibre. Non ho ancora 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 passare da una all'altra come desiderato. Poiché il codice arbitrario può essere eseguito in una fibra, è possibile chiamare il codice di terze parti su una fibra, quindi "bloccarlo" e continuare a fare qualcos'altro quando richiama nel codice che controlli.

Immagina qualcosa del genere: stai scrivendo un programma server che servirà molti client. Un'interazione completa con un client implica l'esecuzione di una serie di passaggi, ma ogni connessione è transitoria e devi ricordare lo stato di ogni client tra le connessioni. (Suona come la programmazione web?)

Invece di memorizzare esplicitamente quello stato e controllarlo ogni volta che un client si connette (per vedere quale è il "passaggio" successivo che deve fare), è possibile mantenere una fibra per ogni client. Dopo aver identificato il cliente, dovresti recuperare la sua fibra e riavviarlo. Quindi, alla fine di ogni connessione, sospendere la fibra e memorizzarla di nuovo. In questo modo, potresti scrivere codice lineare per implementare tutta la logica per un'interazione completa, inclusi tutti i passaggi (proprio come faresti naturalmente se il tuo programma fosse fatto 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 ancora una volta sto solo cercando di mostrarti alcune delle possibilità. Chissà; una volta capito il concetto, potresti trovare un'applicazione totalmente nuova a cui nessun altro ha ancora pensato!


Grazie per la tua risposta! Allora perché non implementano charso altri enumeratori con solo chiusure?
fl00r

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

13
Questa risposta è così buona che dovrebbe essere scritta come un post sul blog da qualche parte, mi sembra.
Jason Voegele

1
AGGIORNAMENTO: Sembra Enumerableche includerà alcuni metodi "pigri" in Ruby 2.0.
Alex D

2
takenon richiede una fibra. Invece, takesi rompe semplicemente durante la resa n-esima. Quando viene utilizzato all'interno di un blocco, breakrestituisce il controllo al frame che definisce il blocco. a = [] ; InfiniteSeries.new.each { |x| a << x ; break if a.length == 10 } ; a
Matteo

22

A differenza delle chiusure, che hanno un punto di entrata e di uscita definito, le fibre possono preservare il loro stato e ritorno (rendimento) 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

stampa questo:

some code
return
received param: param
etc

L'implementazione di questa logica con altre funzionalità di ruby ​​sarà meno leggibile.

Con questa funzione, un buon utilizzo delle fibre consiste nella pianificazione cooperativa manuale (come sostituzione dei fili). Ilya Grigorik ha un buon esempio su come trasformare una libreria asincrona ( eventmachinein questo caso) in quella che sembra un'API sincrona senza perdere i vantaggi della pianificazione IO dell'esecuzione asincrona. Ecco il link .


Grazie! Ho letto documenti, quindi capisco tutta questa magia con molte entrate e uscite all'interno della fibra. Ma non sono sicuro che questa roba renda la vita più facile. Non credo sia una buona idea cercare di seguire tutto questo curriculum e rese. Sembra una bugna difficile da districare. Quindi voglio capire se ci sono casi in cui questa bugna di fibre è una buona soluzione. Eventmachine è interessante ma non è il posto migliore per capire le fibre, perché prima dovresti capire tutte queste cose sullo schema del reattore. Quindi credo di poter capire le fibre physical meaningin un esempio più semplice
fl00r
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.