Pourquoi avons-nous besoin de fibres

101

Pour les fibres, nous avons un exemple classique: la génération de nombres de Fibonacci

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

Pourquoi avons-nous besoin de fibres ici? Je peux réécrire cela avec juste le même Proc (fermeture, en fait)

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

Alors

10.times { puts fib.resume }

et

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

retournera juste le même résultat.

Alors, quels sont les avantages des fibres. Quel genre de trucs que je peux écrire avec Fibers que je ne peux pas faire avec les lambdas et autres fonctionnalités intéressantes de Ruby?

fl00r
la source
4
Le vieil exemple de fibonacci est juste le pire facteur de motivation possible ;-) Il existe même une formule que vous pouvez utiliser pour calculer n'importe quel nombre de fibonacci dans O (1).
usr
17
Le problème ne concerne pas l'algorithme, mais la compréhension des fibres :)
fl00r

Réponses:

230

Les fibres sont quelque chose que vous n'utiliserez probablement jamais directement dans le code au niveau de l'application. Il s'agit d'une primitive de contrôle de flux que vous pouvez utiliser pour créer d'autres abstractions, que vous utilisez ensuite dans un code de niveau supérieur.

L'utilisation n ° 1 des fibres dans Ruby est probablement d'implémenter Enumerators, qui sont une classe Ruby de base dans Ruby 1.9. Ce sont incroyablement utiles.

Dans Ruby 1.9, si vous appelez presque n'importe quelle méthode d'itérateur sur les classes principales, sans passer un bloc, elle renverra 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)>

Ce Enumeratorsont des objets Enumerable, et leurs eachméthodes donnent les éléments qui auraient été générés par la méthode iterator originale, si elle avait été appelée avec un bloc. Dans l'exemple que je viens de donner, l'énumérateur renvoyé par reverse_eacha une eachméthode qui donne 3,2,1. L'énumérateur renvoyé par renvoie chars"c", "b", "a" (et ainsi de suite). MAIS, contrairement à la méthode iterator d'origine, l'énumérateur peut également retourner les éléments un par un si vous l'appelez nextà plusieurs reprises:

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"

Vous avez peut-être entendu parler des «itérateurs internes» et des «itérateurs externes» (une bonne description des deux est donnée dans le livre «Gang of Four» Design Patterns). L'exemple ci-dessus montre que les énumérateurs peuvent être utilisés pour transformer un itérateur interne en un itérateur externe.

Voici une façon de créer vos propres recenseurs:

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

Essayons:

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

Attendez une minute ... est-ce que quelque chose vous semble étrange? Vous avez écrit les yieldinstructions sous an_iteratorforme de code linéaire, mais l'énumérateur peut les exécuter une par une . Entre les appels à next, l'exécution de an_iteratorest "gelée". Chaque fois que vous appelez next, il continue de fonctionner jusqu'à l' yieldinstruction suivante , puis "se fige" à nouveau.

Pouvez-vous deviner comment cela est mis en œuvre? L'énumérateur encapsule l'appel an_iteratordans une fibre et transmet un bloc qui suspend la fibre . Ainsi, chaque fois que an_iteratorle bloc cède, la fibre sur laquelle il fonctionne est suspendue et l'exécution se poursuit sur le thread principal. La prochaine fois que vous appelez next, il passe le contrôle à la fibre, le bloc revient et an_iteratorcontinue là où il s'était arrêté.

Il serait instructif de penser à ce qu'il faudrait pour faire cela sans fibres. CHAQUE classe qui voulait fournir des itérateurs internes et externes devrait contenir du code explicite pour garder une trace de l'état entre les appels à next. Chaque appel à next devrait vérifier cet état et le mettre à jour avant de renvoyer une valeur. Avec les fibres, nous pouvons convertir automatiquement n'importe quel itérateur interne en un itérateur externe.

Cela n'a pas à voir avec les fibres persay, mais permettez-moi de mentionner une autre chose que vous pouvez faire avec les Enumerators: ils vous permettent d'appliquer des méthodes Enumerable d'ordre supérieur à d'autres itérateurs autres que each. Pensez-y: normalement toutes les méthodes, y compris Enumerable map, select, include?, injectet ainsi de suite, tous les travaux sur les éléments produits par each. Mais que faire si un objet a d'autres itérateurs autres que 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]

L'appel de l'itérateur sans bloc renvoie un Enumerator, puis vous pouvez appeler d'autres méthodes Enumerable à ce sujet.

Pour en revenir aux fibres, avez-vous utilisé la takeméthode d'Enumerable?

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

Si quelque chose appelle cette eachméthode, il semble qu'elle ne devrait jamais revenir, non? Regarde ça:

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

Je ne sais pas si cela utilise des fibres sous le capot, mais c'est possible. Les fibres peuvent être utilisées pour implémenter des listes infinies et une évaluation paresseuse d'une série. Pour un exemple de certaines méthodes paresseuses définies avec Enumerators, j'en ai défini quelques-unes ici: https://github.com/alexdowad/showcase/blob/master/ruby-core/collections.rb

Vous pouvez également créer une installation de coroutine à usage général en utilisant des fibres. Je n'ai encore jamais utilisé de coroutines dans aucun de mes programmes, mais c'est un bon concept à connaître.

J'espère que cela vous donne une idée des possibilités. Comme je l'ai dit au début, les fibres sont une primitive de contrôle de flux de bas niveau. Ils permettent de maintenir plusieurs «positions» de flux de contrôle au sein de votre programme (comme différents «signets» dans les pages d'un livre) et de basculer entre elles à volonté. Puisque du code arbitraire peut s'exécuter dans une fibre, vous pouvez appeler du code tiers sur une fibre, puis le «figer» et continuer à faire autre chose lorsqu'il rappelle le code que vous contrôlez.

Imaginez quelque chose comme ceci: vous écrivez un programme serveur qui desservira de nombreux clients. Une interaction complète avec un client implique de passer par une série d'étapes, mais chaque connexion est transitoire et vous devez vous souvenir de l'état de chaque client entre les connexions. (Cela ressemble à de la programmation Web?)

Plutôt que de stocker explicitement cet état et de le vérifier à chaque fois qu'un client se connecte (pour voir quelle est la prochaine "étape" qu'il doit faire), vous pouvez maintenir une fibre pour chaque client. Après avoir identifié le client, vous récupéreriez sa fibre et la redémarreriez. Ensuite, à la fin de chaque connexion, vous suspendriez la fibre et la stockiez à nouveau. De cette façon, vous pouvez écrire du code en ligne droite pour implémenter toute la logique d'une interaction complète, y compris toutes les étapes (comme vous le feriez naturellement si votre programme était conçu pour s'exécuter localement).

Je suis sûr qu'il y a de nombreuses raisons pour lesquelles une telle chose n'est peut-être pas pratique (du moins pour le moment), mais encore une fois, j'essaie simplement de vous montrer certaines des possibilités. Qui sait; une fois que vous avez compris le concept, vous pouvez créer une application totalement nouvelle à laquelle personne d'autre n'a encore pensé!

Alex D
la source
Merci pour votre réponse! Alors pourquoi ne mettent-ils pas en œuvre charsou d'autres agents recenseurs avec juste des fermetures?
fl00r
@ fl00r, je pense ajouter encore plus d'informations, mais je ne sais pas si cette réponse est déjà trop longue ... en voulez-vous plus?
Alex D
13
Cette réponse est si bonne qu'elle devrait être écrite comme un article de blog quelque part, me semble-t-il.
Jason Voegele
1
MISE À JOUR: Il semblerait que Enumerablecertaines méthodes "paresseuses" soient incluses dans Ruby 2.0.
Alex D
2
takene nécessite pas de fibre. Au lieu de cela, takese casse simplement pendant le nième rendement. Lorsqu'il est utilisé à l'intérieur d'un bloc, breakrenvoie le contrôle au cadre définissant le bloc. a = [] ; InfiniteSeries.new.each { |x| a << x ; break if a.length == 10 } ; a
Matthew le
22

Contrairement aux fermetures, qui ont un point d'entrée et de sortie défini, les fibres peuvent conserver leur état et revenir (céder) plusieurs fois:

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

imprime ceci:

some code
return
received param: param
etc

L'implémentation de cette logique avec d'autres fonctionnalités ruby ​​sera moins lisible.

Avec cette fonctionnalité, une bonne utilisation des fibres consiste à effectuer une planification coopérative manuelle (en remplacement des threads). Ilya Grigorik a un bon exemple sur la façon de transformer une bibliothèque asynchrone ( eventmachinedans ce cas) en ce qui ressemble à une API synchrone sans perdre les avantages de la planification IO de l'exécution asynchrone. Voici le lien .

Aliaksei Kliuchnikau
la source
Je vous remercie! J'ai lu des documents, donc je comprends toute cette magie avec de nombreuses entrées et sorties à l'intérieur de la fibre. Mais je ne suis pas sûr que ce genre de choses facilite la vie. Je ne pense pas que ce soit une bonne idée d'essayer de suivre tout cela reprend et donne. Cela ressemble à un point d'écoute difficile à démêler. Je veux donc comprendre s'il y a des cas où ce point d'écoute de fibres est une bonne solution. Eventmachine est cool mais ce n'est pas le meilleur endroit pour comprendre les fibres, car vous devez d'abord comprendre toutes ces choses sur le modèle de réacteur. Donc je crois que je peux comprendre les fibres physical meaningdans un exemple plus simple
fl00r