Synchronisation von Threads

Bald wirst Du ausreichend vertraut damit sein, live mit einer Anzahl von gleichzeitig ablaufenden Funktionen und Threads zu coden. Dann wirst Du bemerken, dass es ziemlich leicht ist, einen einzelnen Thread mit einem Fehler zum Abbruch zu bringen. Das ist nicht weiter schlimm, da Du den Thread mit einem Klick auf Ausführen ja einfach neu starten kannst. Wenn Du den Thread aber neu startest, dann läuft er nicht mehr zusammen mit den anderen so wie vorher, er ist nun aus dem Takt mit den ursprünglichen Threads.

Vererbte Zeit

Wir haben zuvor erwähnt, dass neue Threads beim Start mit in_thread alle ihre Einstellungen vom Eltern-Thread erben. Das schließt auch die aktuelle Zeit mit ein. Das bedeutet, dass Threads bei gleichzeitigem Start immer miteinander im Takt sind.

Wenn Du aber einen Thread alleine startest, spielt er in seinem eigenen Takt, der höchstwahrscheinlich nicht mit irgendeinem der anderen laufenden Threads zusammenpasst.

Cue und Sync

Sonic Pi hat mit den Funktionen cue und sync eine Lösung für dieses Problem.

cue erlaubt es uns, regelmäßige Signale an alle anderen Threads mit einem Taktgeber zu versenden. Normalerweise sind die anderen Threads an solchen Signale nicht interessiert und werden sie ignorieren. Mit der sync-Funktion kann man jedoch erreichen, dass ein anderer Thread auf diesen Taktgeber hört.

Eine wichtige Sache, derer man sich bewusst sein sollte: sync ist so ähnlich sleep, weil es den aktuellen Thread für eine bestimmte Zeit anhält und dieser so lange nichts tut. Allerdings sagt man mit sleep, wie lange man warten möchte, während man bei sync nicht weiß, wie lange gewartet wird - denn sync wartet auf das nächste cue eines anderen Threads; das kann kürzer oder länger dauern.

Sehen wir uns das etwas genauer an:

in_thread do
  loop do
    cue :tick
    sleep 1
  end
end
in_thread do
  loop do
    sync :tick
    sample :drum_heavy_kick
  end
end

Hier haben wir zwei Threads - einer davon arbeitet wie ein Metronom; er spielt nichts, aber sendet bei jedem Schlag ein :tick-Taktgebersignal. Der zweite Thread synchronisiert sich mit den tick-Signalen, und wenn er eins davon erhält, übernimmt er die Zeit vom cue-Thread und läuft weiter.

Als Ergebnis hören wir den :drum_heavy_kick Sample genau dann, wenn der andere Thread ein :tick-Signal sendet, auch dann, wenn beide Threads gar nicht zur selben Zeit gestartet sind:

in_thread do
  loop do
    cue :tick
    sleep 1
  end
end
sleep(0.3)
in_thread do
  loop do
    sync :tick
    sample :drum_heavy_kick
  end
end

Dieser freche (zweite) Aufruf von sleep würde eigentlich den zweiten Thread gegenüber dem ersten aus dem Takt bringen. Weil wir aber cue und sync verwenden, synchronisieren wir beide Threads automatisch und umgehen dabei, dass beide in der Zeit auseinanderfallen.

Cue benennen

Du kannst cue-Signale benennen, wie Du willst - nicht nur mit :tick. Du musst nur sicherstellen, dass irgendein anderer Thread sich mit eben diesem Namen synchronisiert - ansonsten wartet der zweite Thread endlos (oder jedenfalls bis Du den Stopp-Button klickst).

Lasst uns ein paar Namen für cue ausprobieren:

in_thread do
  loop do 
    cue [:foo, :bar, :baz].choose
    sleep 0.5
  end
end
in_thread do
  loop do 
    sync :foo 
    sample :elec_beep
  end
end
in_thread do
  loop do
    sync :bar
    sample :elec_flip
  end
end
in_thread do
  loop do
    sync :baz
    sample :elec_blup
  end
end

Hier haben wir eine cue-Hauptschleife, die auf Zufallsbasis einen von drei Namen des Taktgebers :foo, :bar oder :baz aussendet. Wir haben auch drei Schleifen-Threads, die jeder für sich mit einem der Namen synchronisiert werden und dann jeweils ein anderes Sample spielen. Im Endeffekt hören wir bei jedem halben Schlag einen Klang, da jeder der sync-Threads auf Zufallsbasis mit dem cue-Thread synchronisiert ist, und sein Sample spielt, wenn er dran ist.

Das funktioniert natürlich auch, wenn Du die Reihenfolge der Threads umkehrst: Die sync-Threads sitzen einfach da und warten auf ihren nächsten cue.