Warum brauchen wir Fasern?

100

Für Fasern haben wir ein klassisches Beispiel: Generieren von Fibonacci-Zahlen

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

Warum brauchen wir hier Fasern? Ich kann dies mit genau dem gleichen Proc umschreiben (Abschluss eigentlich)

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

So

10.times { puts fib.resume }

und

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

gibt genau das gleiche Ergebnis zurück.

Was sind die Vorteile von Fasern? Was für Sachen kann ich mit Fasern schreiben, die ich mit Lambdas und anderen coolen Ruby-Funktionen nicht machen kann?

fl00r
quelle
4
Das alte Fibonacci-Beispiel ist nur der schlechteste Motivator ;-) Es gibt sogar eine Formel, mit der Sie jede Fibonacci-Zahl in O (1) berechnen können .
usr
17
Das Problem ist nicht über Algorithmus, sondern über das Verständnis von Fasern :)
fl00r

Antworten:

229

Fasern werden Sie wahrscheinlich nie direkt im Code auf Anwendungsebene verwenden. Sie sind ein Flusssteuerungsprimitiv, mit dem Sie andere Abstraktionen erstellen können, die Sie dann in übergeordnetem Code verwenden.

Wahrscheinlich besteht die häufigste Verwendung von Fasern in Ruby darin, Enumerators zu implementieren , die eine Ruby-Kernklasse in Ruby 1.9 sind. Diese sind unglaublich nützlich.

Wenn Sie in Ruby 1.9 fast jede Iteratormethode für die Kernklassen aufrufen, ohne einen Block zu übergeben, wird eine zurückgegeben 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)>

Dies Enumeratorsind Enumerable-Objekte, und ihre eachMethoden liefern die Elemente, die von der ursprünglichen Iterator-Methode erhalten worden wären, wenn sie mit einem Block aufgerufen worden wäre. In dem Beispiel, das ich gerade gegeben habe, hat der von zurückgegebene Enumerator reverse_eacheine eachMethode, die 3,2,1 ergibt. Der Enumerator gab die charsAusbeuten "c", "b", "a" (und so weiter) zurück. ABER im Gegensatz zur ursprünglichen Iteratormethode kann der Enumerator die Elemente auch einzeln zurückgeben, wenn Sie nextsie wiederholt aufrufen :

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"

Möglicherweise haben Sie von "internen Iteratoren" und "externen Iteratoren" gehört (eine gute Beschreibung von beiden finden Sie im Buch "Gang of Four" -Designmuster). Das obige Beispiel zeigt, dass Enumeratoren verwendet werden können, um einen internen Iterator in einen externen zu verwandeln.

Dies ist eine Möglichkeit, eigene Enumeratoren zu erstellen:

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

Lass es uns versuchen:

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

Moment mal ... scheint da etwas seltsam? Sie schrieb die yieldAussagen in an_iteratorals geradlinigen Code, aber der Enumerator kann sie läuft einen nach dem anderen . Zwischen den Aufrufen von nextist die Ausführung von an_iterator"eingefroren". Jedes Mal next, wenn Sie aufrufen , wird die folgende yieldAnweisung ausgeführt und "friert" erneut ein.

Können Sie sich vorstellen, wie dies umgesetzt wird? Der Enumerator umschließt den Aufruf mit an_iteratoreiner Faser und übergibt einen Block, der die Faser unterbricht . Jedes Mal, wenn an_iteratorder Block nachgibt, wird die Faser, auf der er ausgeführt wird, angehalten und die Ausführung im Hauptthread fortgesetzt. Beim nächsten Aufruf nextwird die Steuerung an die Glasfaser übergeben, der Block kehrt zurück und wird dort an_iteratorfortgesetzt, wo er aufgehört hat.

Es wäre lehrreich darüber nachzudenken, was erforderlich wäre, um dies ohne Fasern zu tun. JEDE Klasse, die sowohl interne als auch externe Iteratoren bereitstellen wollte, musste expliziten Code enthalten, um den Status zwischen den Aufrufen von zu verfolgen next. Jeder Aufruf zum nächsten müsste diesen Status überprüfen und aktualisieren, bevor ein Wert zurückgegeben wird. Mit Fasern können wir jeden internen Iterator automatisch in einen externen konvertieren.

Dies hat nichts mit Fasern zu tun, aber ich möchte noch eines erwähnen, das Sie mit Enumeratoren tun können: Sie ermöglichen es Ihnen, Enumerable-Methoden höherer Ordnung auf andere Iteratoren als anzuwenden each. Denken Sie daran: normalerweise alle Enumerable - Methoden, einschließlich map, select, include?, inject, und so weiter, alle Arbeiten an den Elementen erhalten durch each. Aber was ist, wenn ein Objekt andere Iteratoren als hat 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]

Wenn Sie den Iterator ohne Block aufrufen, wird ein Enumerator zurückgegeben. Anschließend können Sie andere Enumerable-Methoden aufrufen.

Zurück zu den Fasern, haben Sie die takeMethode von Enumerable verwendet?

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

Wenn irgendetwas diese eachMethode aufruft, sieht es so aus, als sollte sie niemals zurückkehren, oder? Überprüfen Sie dies heraus:

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

Ich weiß nicht, ob hier Fasern unter der Haube verwendet werden, aber es könnte sein. Fasern können verwendet werden, um unendliche Listen und eine verzögerte Auswertung einer Reihe zu implementieren. Als Beispiel für einige mit Enumerators definierte Lazy-Methoden habe ich hier einige definiert: https://github.com/alexdowad/showcase/blob/master/ruby-core/collections.rb

Sie können auch eine Allzweck-Coroutine-Anlage aus Fasern bauen. Ich habe noch nie Coroutinen in einem meiner Programme verwendet, aber es ist ein gutes Konzept zu wissen.

Ich hoffe, dies gibt Ihnen eine Vorstellung von den Möglichkeiten. Wie ich zu Beginn sagte, sind Fasern ein Grundelement für die Flusskontrolle auf niedriger Ebene. Sie ermöglichen es, mehrere Kontrollfluss- "Positionen" in Ihrem Programm beizubehalten (wie verschiedene "Lesezeichen" auf den Seiten eines Buches) und nach Bedarf zwischen ihnen zu wechseln. Da beliebiger Code in einer Glasfaser ausgeführt werden kann, können Sie Code von Drittanbietern auf einer Glasfaser aufrufen, ihn dann "einfrieren" und etwas anderes tun, wenn er den von Ihnen kontrollierten Code zurückruft.

Stellen Sie sich so etwas vor: Sie schreiben ein Serverprogramm, das viele Clients bedient. Eine vollständige Interaktion mit einem Client umfasst eine Reihe von Schritten, aber jede Verbindung ist vorübergehend, und Sie müssen sich den Status für jeden Client zwischen den Verbindungen merken. (Klingt nach Webprogrammierung?)

Anstatt diesen Status explizit zu speichern und jedes Mal zu überprüfen, wenn ein Client eine Verbindung herstellt (um zu sehen, was der nächste "Schritt" ist, den er tun muss), können Sie für jeden Client eine Glasfaser verwalten. Nachdem Sie den Client identifiziert haben, rufen Sie dessen Glasfaser ab und starten ihn neu. Am Ende jeder Verbindung würden Sie die Glasfaser aussetzen und erneut speichern. Auf diese Weise können Sie geradlinigen Code schreiben, um die gesamte Logik für eine vollständige Interaktion einschließlich aller Schritte zu implementieren (genau wie Sie es natürlich tun würden, wenn Ihr Programm lokal ausgeführt würde).

Ich bin mir sicher, dass es viele Gründe gibt, warum so etwas (zumindest für den Moment) nicht praktikabel ist, aber ich versuche nur, Ihnen einige der Möglichkeiten aufzuzeigen. Wer weiß; Sobald Sie das Konzept erhalten haben, können Sie sich eine völlig neue Anwendung einfallen lassen, an die noch niemand gedacht hat!

Alex D.
quelle
Vielen Dank für Ihre Antwort! Warum implementieren sie keine charsanderen Enumeratoren mit nur Abschlüssen?
Fl00r
@ fl00r, ich denke darüber nach, noch mehr Informationen hinzuzufügen, aber ich weiß nicht, ob diese Antwort bereits zu lang ist ... willst du mehr?
Alex D
13
Diese Antwort ist so gut, dass sie irgendwo als Blog-Post geschrieben werden sollte.
Jason Voegele
1
UPDATE: Es sieht so aus, Enumerableals würde Ruby 2.0 einige "faule" Methoden enthalten.
Alex D
2
takebenötigt keine Faser. Bricht stattdessen takeeinfach während der n-ten Ausbeute. Bei Verwendung innerhalb eines Blocks wird breakdie Kontrolle an den Frame zurückgegeben, der den Block definiert. a = [] ; InfiniteSeries.new.each { |x| a << x ; break if a.length == 10 } ; a
Matthew
22

Im Gegensatz zu Verschlüssen, die einen definierten Eintritts- und Austrittspunkt haben, können Fasern ihren Zustand beibehalten und viele Male zurückkehren (nachgeben):

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

druckt dies:

some code
return
received param: param
etc

Die Implementierung dieser Logik mit anderen Ruby-Funktionen ist weniger lesbar.

Mit dieser Funktion ist eine gute Fasernutzung die manuelle kooperative Planung (als Thread-Ersatz). Ilya Grigorik hat ein gutes Beispiel dafür, wie eine asynchrone Bibliothek ( eventmachinein diesem Fall) in eine synchrone API umgewandelt werden kann, ohne die Vorteile der E / A-Planung der asynchronen Ausführung zu verlieren. Hier ist der Link .

Aliaksei Kliuchnikau
quelle
Danke dir! Ich lese Dokumente, also verstehe ich all diese Magie mit vielen Ein- und Ausgängen innerhalb der Faser. Aber ich bin mir nicht sicher, ob dieses Zeug das Leben leichter macht. Ich denke nicht, dass es eine gute Idee ist, all diesen Lebensläufen und Erträgen zu folgen. Es sieht aus wie eine Schlaufe, die schwer zu entwirren ist. Ich möchte also verstehen, ob es Fälle gibt, in denen dieser Faserspalt eine gute Lösung ist. Eventmachine ist cool, aber nicht der beste Ort, um Fasern zu verstehen, denn zuerst sollten Sie all diese Dinge des Reaktormusters verstehen. Ich glaube also, ich kann Fasern physical meaningin einem einfacheren Beispiel verstehen
fl00r