next up previous contents index
Search Next: Prozesse und Threads Up: Einführung in Ruby Previous: Ausnahme- und Fehlerbehandlung   Contents   Index

Continuations

It's a kind of magic.
Connor MacLeod


\epsfig{height=24pt,file=images/ruby.eps}Dieses Kapitel hat Clemens Hintze geschrieben, ein europäischer Ruby-Fan der ersten Stunde (sein Name taucht schon in der ersten Nachricht auf der ruby-talk-Mailingliste auf).

Das Konzept der Continuations ist so verschieden von den herkömlichen Programmstrukturen wie Schleifen und Verzweigungen, dass man ihr Anwendungspotenzial nicht immer sofort erkennt. Dazu gehört einiges an Erfahrung und der Mut, Continuations immer als Möglichkeit in Erwägung zu ziehen. In Ruby lassen sich ihre Eigenschaften und ihre daraus resultierenden Verwendungsmöglichkeiten mit nur einem Wort treffend beschreiben: Magie!

Continuations sind keine Erfindung von Ruby, es gibt sie zum Beispiel auch in Scheme. Da die Eigenschaften von Continuations nicht in allen Sprachen gleich sind, betonen wir ausdrücklich, dass es in diesem Abschnitt nur um die Ruby-Variante von Continuations geht.

Was[*] sind Continuations? Die Übersetzung des englischen Begriffs gibt schon eine vage Vorstellung - Continuation bedeutet Fortsetzung, in unserem Zusammenhang ist der Begriff Fortsetzungspunkt besser. Um das eigentlich einfache Konzept der Continuations erfassen zu können, muss man zuerst verstehen, was bei der Abarbeitung eines Programms im Grunde passiert.

Prinzipiell besteht ein Programm aus Aufrufen von Unterprogrammen (Funktionen in C, Methoden oder Codeblöcke in Ruby), die wiederum Unterprogramme aufrufen, bis die unterste Ebene erreicht ist - die Ebene, in der die eigentliche Arbeit in einer Folge von Maschinenbefehlen getan wird.

Wenn[*] nun eine Methode ausgeführt wird, läuft sie in ihrem Aktivierungskontext ab. Dort befinden sich unter anderem ihre lokalen Variablen (Datenkontext) und die Aufrufkette (Callchain), die zur Aktivierung der Methode geführt hat. Hat die Methode ihre Arbeit beendet, wird der Kontext wieder zerstört.

Ruby erlaubt es nun, auf den gerade aktuell gültigen Kontext zuzugreifen und ihn vor der Zerstörung zu bewahren.

Die[*] Kernel-Methode binding kapselt den Datenkontext in einer Instanz der Klasse Binding und ermöglicht so die Weiterverwendung auch nach dem Verlassen der Methode.

cmd = %q( puts "n=#{n}, s='#{s}'" )
def foo(n)
  s = "foo mit n+1=#{n+1}"
  binding
end
b1 = foo(12)
b2 = foo(14)

b1.class        #-> Binding
eval(cmd, b1)   #-> n=12, s='foo mit n+1=13'
eval(cmd, b2)   #-> n=14, s='foo mit n+1=15'

Nachdem der Datenkontext der Methode foo jeweils in b1 und b2 konserviert wurde, wird die in cmd gespeicherte puts-Anweisung im jeweiligen entsprechenden Kontext ausgeführt.

Die[*]Kernel-Methode caller liefert ein Array von Strings zurück, welches die Aufrufkette beschreibt. Dieser Teil des Kontextes kann nur zur Visualisierung verwendet werden, er steht also nach dem Verlassen der Methode nicht mehr zur Verfügung.

def foo1; foo2; end
def foo2; puts caller(0); end

foo1   #-> -:2:in `foo2'
       #-> -:1:in `foo1'

Von dem bisher Gesagten[*] ist es nur noch ein winziger Schritt zu den Continuations. Wir haben eine Methode, um den Datenanteil des Kontextes zu archivieren, und wir haben eine Methode, um die Aufrufkette zu visualisieren. Was uns noch für eine Continuation fehlt, ist, einen kompletten Kontext zwecks späterer Verwendung zu speichern.

Die Kernel-Methode callcc erzeugt eine[*] Instanz der Klasse Continuation. Diese Methode erwartet einen einmalig auszuführenden Codeblock, dem die Continuation als Argument übergeben wird.

callcc[*]kann mehr als einmal zurückkehren oder auch gar nicht! Wenn der von callcc ausgeführte Block beendet wird, gibt callcc den Wert seines letzten Ausdruckes zurück. Wird der konservierte Kontext mittels call reaktiviert, wird der Block von callcc nicht nochmals ausgeführt: callcc gibt nur die Argumente zurück, die beim aktuellen Aufruf von call angegeben wurden, nil falls keine Argumente verwendet wurden.

[*]Der Kontext wird nahezu vollständig reaktiviert: Es wird sozusagen eine Zeitreise zurück angetreten und das Programm findet sich an der Stelle des callcc-Aufrufs wieder, allerdings schon um einige Erfahrungen reicher.

Ein paar Beispiele machen dies deutlicher:

i = 1
callcc { |$cc| puts "Höre ich mehr?"}
puts "zum #{i}-ten "
i += 1
$cc.call if i <= 3
puts "... und verkauft!"

Effektiv werden folgende Anweisungen abgearbeitet:

i = 1
$cc = ...   # Speicherung des Kontexts
puts "Höre ich mehr?"
puts "zum #{i}-ten "  # i == 1
i += 1
puts "zum #{i}-ten "  # i == 2
i += 1
puts "zum #{i}-ten "  # i == 3
puts "... und verkauft!"

Wir sehen, dass die Continuation $cc die Bindung zur Variablen i enthält, und nicht nur den zum Zeitpunkt der Definition aktuellen Wert. Wird i an ein anderes Objekt gebunden, ist dies auch im gespeicherten Kontext der Continuation sichtbar.

Nun[*] könnte man das obige Beispiel auch gut mittels goto realisieren, wenn Ruby so etwas hätte. Während aber goto üblicherweise nur eine Sprunganweisung ist, wird mit Continuations ein vorheriger, vielleicht sogar bereits abgeschlossener Kontext wiederhergestellt, in dem der Code ausgeführt wird. Als Nebeneffekt findet man sich dann auch an einer anderen Stelle im Code wieder - der Stelle, an der die Continuation kreiert wurde. So kann man zum Beispiel sogar in eine eigentlich schon abgeschlossene Schleife innerhalb einer beendeten Methode zurückspringen!

class Fixnum
  def urlaubstage_in(ort, &tuwas)
    puts "Koffer packen und ab nach #{ort}"
    (1..self).each &tuwas
    puts "jetzt geht's wieder heim"
  end
end

# nur einmal nach Tahiti ...
5.urlaubstage_in 'Tahiti' do |tag|
  puts "  #{tag}. Tag: baden ..."
end

Wir können nun für fünf Tage Urlaub nehmen und an jedem Tag der Tätigkeit nachgehen (Codeblock), die wir mögen. Leider ist der Urlaub bald zu Ende und unser täglich Einerlei beginnt aufs Neue ... oder nicht? Ruby macht es möglich,[*]immer wieder zum ersten Urlaubstag zurückzukehren:

urlaub = nil
5.urlaubstage_in 'Tahiti' do |tag|
  callcc { |urlaub| } unless urlaub  # (1)
  puts "\t\t#{tag}. Tag: baden ..."
end
urlaub.call                          # (2)

Bemerkenswert ist, wie wir uns in einer Zeitschleife von (1) zu (2) zu (1) und so weiter bewegen. Gerade noch zu Hause angekommen, befinden wir uns in der nächsten Sekunde wieder auf Tahiti und genießen weitere fünf lange Tage am Strand, um wieder nach Hause zu fliegen und in der nächsten Sekunde ...

Mittels urlaub.call (2) gelangten wir nach (1) zurück, zu dem Zeitpunkt, als der Block unter Ausführung von Range#each stand, das seinerseits gerade von Fixnum#urlaubstage_in ausgeführt wurde.

Abschließend[*] noch ein reales und nützliches Beispiel - Koroutinen.

Einige Sprachen, wie z.B. Modula 2, kennen das Konzept von Koroutinen. Als Koroutinen werden dort Unterprogramme (Procedure) verstanden, die in einem eigenen Stackkontext ablaufen. Einmal gestartet[*], laufen sie so lange, bis sie die Flusskontrolle freiwillig an eine andere bestimmte Koroutine abgeben - eine Art kooperatives Multithreading.

Jedoch kann eine Koroutine die Kontrolle auch nur unter der Auflage abgeben, dass sie im Falle[*] des Eintreffens eines bestimmten Signals die Kontrolle sofort und bedingungslos zurückerhält. Es sei denn, eine andere Koroutine hat sich ebenfalls für dieses Signal angemeldet: Wer zuletzt kommt, wird bedient.

Wozu Koroutinen, wenn Ruby doch Threads hat? Koroutinen verursachen weniger Synchronisationsaufwand, und wenn man will, kann man ein eigenes Multithreading mittels Koroutinen realisieren und dabei verschiedene Schedulerstrategien ausprobieren.

Da es um Speichern und Wiederherstellen von Kontexten geht, ist auch dieses Problem ein klarer Fall für Continuations. Für unser Beispiel wählen wir eine OO-Lösung und definieren eine Klasse Coroutine mit folgenden öffentlichen Methoden:

Den transfer_to-Methoden kann optional eine Signalkennzeichnung mitgegeben werden, so dass beim Auftreten dieses Signals der vorgenommene Transfer rückgängig gemacht wird.

Und[*] hier die Implementation der Klasse:

class Coroutine
  class Error < Exception; end
  @@main = nil
  def initialize(&block)
    @block = block || method(:run)
  end
  def transfer_to(to, signal=nil)
    if signal
      trap(signal) do
        trap(signal, "DEFAULT")
        to.transfer_to self
      end
    end
    to.activate if self.saved?
  end
  def suspend
    self.transfer_to @@main
  end
  def Coroutine.transfer_to(to, signal=nil)
    @@main = Coroutine.new {}
    @@main.transfer_to(to, signal)
  end
protected
  def activate
    if @cont then 
      @cont.call false
    else 
      @block.call
      raise Error, "coroutine block was left!"
    end
  end
  def saved?
    callcc { |@cont| true }
  end
end

Die eigentliche Arbeit wird von den beiden als protected deklarierten Methoden activate und saved? geleistet, die von den transfer_to-Methoden verwendet werden.

Mit activate wird der gespeicherte Kontext der Koroutine reaktiviert oder gestartet. saved? speichert den aktuellen Kontext der Koroutine und kehrt mit true zurück, wenn die Konservierung erfolgreich war. saved? kehrt bei Reaktivierungen mit false zurück, da keine Speicherung des Kontextes erfolgte.

Es gibt zwei Möglichkeiten, eine Koroutine zu kreieren:

Hier[*] ein Beispiel zweier Koroutinen, die sich gegenseitig aktivieren, bis die Sequenz mit Strg+C abgebrochen wird.

require "coroutine"

class Foo < Coroutine
  attr_accessor :next
  def initialize(msg)
    super()               # Wichtig!
    @msg = msg
  end
  def run
    loop do
      puts @msg
      transfer_to @next
    end
  end
end

foo = Foo.new("Foo")
bar = Coroutine.new do 
  loop { puts "bar"; bar.transfer_to foo }
end
foo.next = bar

Coroutine.transfer_to foo, "SIGINT"
puts "User requested abort!"

Und[*] noch ein weiteres Beispiel, in dem wir ein einfaches Multithreading implementieren. Koroutinen werden kreiert und bei einem Scheduler angemeldet, der bei jedem Strg+C zur nächsten Koroutine umschaltet. Der Scheduler bricht ab, nachdem zweimal durch alle Koroutinen durchgeschaltet wurde.

require "coroutine"

class Scheduler
  def initialize
    @tasks = Array.new
  end
  def add(&block)
    @tasks << Coroutine.new(&block)
  end
  def start
    2.times do
      @tasks.each do |task|
        Coroutine.transfer_to task, "SIGINT"
      end
    end
  end
end

sched = Scheduler.new

sched.add { loop { puts "Foo" } }
sched.add { loop { puts "Bar" } }
sched.add { loop { puts "FooBar" } }

sched.start



Footnotes

...Was[*]
Continuations handhaben Kontexte
...Wenn[*]
Aktivierungskontext = Datenkontext + Aufrufkette
...Die[*]
Kernel::binding -> Datenkontext
...Die[*]
Kernel::caller -> Aufrufkette des Kontextes
... Gesagten[*]
Continuation = kompletter Aktivierungskontext
... eine[*]
Kernel::callcc -> Continuation
... callcc[*]
Rückkehrverhalten von callcc
...P[*]
Wichtig!
...Nun[*]
Continuation $\neq$ Goto
... möglich,[*]
Zeitschleife im Urlaub?
...Abschließend[*]
Reales Beispiel - Koroutinen
... gestartet[*]
Unbedingte Abgabe der Flusskontrolle
... Falle[*]
Abgabe der Flusskontrolle unter Auflage
...Und[*]
Der Code in ganzer Pracht
...Hier[*]
Ein erstes Beispiel
...Und[*]
Ein manueller Scheduler

next up previous contents index
Search Next: Prozesse und Threads Up: Einführung in Ruby Previous: Ausnahme- und Fehlerbehandlung   Contents   Index
(C) 2002 by dpunkt.de, Armin Roehrl, Stefan Schmiedl, Clemens Wyss 2002-01-20