web-dev-qa-db-fra.com

Pourquoi avons-nous besoin de fibres

Pour les fibres, nous avons un exemple classique: 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 le même Proc (fermeture, en fait)

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

Donc

10.times { puts fib.resume }

et

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

retournera exactement le même résultat.

Quels sont donc les avantages des fibres. Quel genre de choses que je peux écrire avec des fibres que je ne peux pas faire avec des lambdas et d'autres fonctionnalités intéressantes Ruby?

94
fl00r

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 du code de niveau supérieur.

Probablement l'utilisation # 1 des fibres dans Ruby est d'implémenter Enumerators, qui sont un noyau Ruby class in Ruby 1.9. Ce sont incroyablement très utiles.

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

Ces Enumerator sont des objets Enumerable, et leurs méthodes each donnent les éléments qui auraient été fournis par la méthode itérative d'origine, si elle avait été appelée avec un bloc. Dans l'exemple que je viens de donner, l'énumérateur est retourné par reverse_each a une méthode each qui donne 3,2,1. L'énumérateur renvoyé par chars renvoie "c", "b", "a" (et ainsi de suite). MAIS, contrairement à la méthode itérative d'origine, l'énumérateur peut également renvoyer les éléments un par un si vous appelez next dessus à 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 d '"itérateurs internes" et d' "itérateurs externes" (une bonne description des deux est donnée dans le livre "Pattern 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.

C'est une façon de créer vos propres énumérateurs:

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 instructions yield dans an_iterator comme code linéaire, mais l'énumérateur peut les exécuter un par un . Entre les appels à next, l'exécution de an_iterator est gelé". Chaque fois que vous appelez next, il continue de s'exécuter jusqu'à l'instruction yield suivante, puis se "fige" à nouveau.

Pouvez-vous deviner comment cela est mis en œuvre? L'énumérateur encapsule l'appel à an_iterator dans une fibre, et passe un bloc qui suspend la fibre . Donc à chaque fois an_iterator cède au bloc, la fibre sur laquelle il s'exécute 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 retourne , et an_iterator continue là où il s'était arrêté.

Il serait instructif de penser à ce qui serait nécessaire pour le faire 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 automatiquement convertir tout itérateur interne en itérateur externe.

Cela n'a rien à voir avec la persistance des fibres, mais permettez-moi de mentionner une autre chose que vous pouvez faire avec les énumérateurs: ils vous permettent d'appliquer des méthodes énumérables d'ordre supérieur à d'autres itérateurs autres que each. Pensez-y: normalement toutes les méthodes Enumerable, y compris map, select, include?, inject, et ainsi de suite, tous travaillent sur les éléments générés 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 énumérateur, puis vous pouvez appeler d'autres méthodes énumérables à ce sujet.

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

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

Si quelque chose appelle cette méthode each, 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 ça pourrait. Les fibres peuvent être utilisées pour implémenter des listes infinies et l'évaluation paresseuse d'une série. Pour un exemple de certaines méthodes paresseuses définies avec des énumérateurs, j'en ai défini ici: https://github.com/alexdowad/showcase/blob/master/Ruby-core/collections.rb

Vous pouvez également construire une installation de coroutine à usage général à l'aide de fibres. Je n'ai encore jamais utilisé de coroutines dans aucun de mes programmes, mais c'est un bon concept à savoir.

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 comme vous le souhaitez. Étant donné que du code arbitraire peut s'exécuter dans une fibre, vous pouvez appeler du code tiers sur une fibre, puis le "geler" 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 conserver une fibre pour chaque client. Après avoir identifié le client, vous récupérez sa fibre et la redémarrez. Ensuite, à la fin de chaque connexion, vous suspendiez la fibre et la stockiez à nouveau. De cette façon, vous pouvez écrire du code linéaire 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 exécuté localement).

Je suis sûr qu'il existe de nombreuses raisons pour lesquelles une telle chose peut ne pas être pratique (au moins pour l'instant), 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 proposer une application totalement nouvelle à laquelle personne d'autre n'a encore pensé!

220
Alex D

Contrairement aux fermetures, qui ont un point d'entrée et de sortie défini, les fibres peuvent conserver leur état et retourner (rendement) 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 (comme remplacement des threads). Ilya Grigorik a un bon exemple sur la façon de transformer une bibliothèque asynchrone (eventmachine dans 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 .

21