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?
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 Enumerator
s, 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é!
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 .