Threads synchronisieren

Sobald du ausreichend vertraut damit bist, mit mehreren gleichzeitig ablaufenden Funktionen und Threads live zu programmieren, wirst du bemerken, dass es ziemlich leicht ist in einem einzelnen Thread einen Fehler zu machen, der ihn zum Absturz bringt. Das ist nicht weiter schlimm, da du den Thread ja mit einem Klick auf Ausführen einfach neu starten kannst. Wenn du den Thread aber neu startest, dann läuft er nicht mehr im Takt mit den anderen Threads.

Vererbte Zeit

Wie wir bereits vorher gesehen haben, erben neue Threads die mit in_thread erzeugt werden alle ihre Einstellungen von einem Eltern-Thread. Das schließt auch die aktuelle Zeit mit ein. Das bedeutet, dass Threads immer miteinander im Takt sind, wenn sie gleichzeitig gestartet werden.

Wenn du aber einen Thread für sich alleine startest, spielt dieser in seinem eigenen Takt, der wahrscheinlich nicht mit irgendeinem der anderen gerade laufenden Threads synchron ist.

Cue und Sync

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

cue erlaubt es uns, mit einem Taktgeber regelmäßig Signale an alle anderen Threads zu versenden. Normalerweise zeigen die anderen Threads an solchen Takt-Signalen kein Interesse und ignorieren sie. Mit der sync-Funktion kann du jedoch erreichen, dass ein anderer Thread Interesse zeigt.

Wichtig ist dabei sich darüber bewusst zu sein, dass sync ähnlich wie sleep funktioniert, indem es den aktuellen Thread für eine bestimmte Dauer anhält. Allerdings legst du bei sleepfest, wie lange du warten willst, während du bei sync nicht weißt, wie lange gewartet werden wird - da sync auf den nächsten cue eines anderen Threads wartet - was eine kürzere oder längere Dauer sein kann.

Sehen wir uns das im Detail 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 arbeitet wie ein Metronom, er spielt nichts, aber sendet bei jedem Schlag das Taktgeber-Signal :tick. Der zweite Thread synchronisiert sich mit den tick-Signalen, und wenn er ein Signal erhält, erbt er dadurch den Takt vom cue-Thread und läuft weiter.

Im Ergebnis hören wir das :drum_heavy_kick-Sample genau dann, wenn der andere Thread das :tick-Signal sendet, auch dann wenn die Ausführung beider Threads gar nicht zur selben Zeit gestartet war:

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

Der dreiste Aufruf von sleep würde normalerweise den zweiten Thread gegenüber dem ersten aus dem Takt bringen. Da wir jedoch cue und sync verwenden, synchronisieren wir beide Threads automatisch und umgehen dabei ungewollte Abweichungen in der Zeit.

Cue-Namen

Du kannst deine cue-Signale benennen, wie du willst - nicht nur mit :tick. Du musst nur sicherstellen, dass jegliche anderen Threads, die sich mit sync synchronisieren, auch diesen Namen verwenden - ansonsten werden sie endlos warten (oder zumindest so lange bis du auf Stopp klickst).

Lass uns mit ein paar Namen für cue spielen:

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 taktgebende cue-Schleife, die auf Zufallsbasis einen der drei Taktgeber-Namen :foo, :bar oder :baz aussendet. Wir haben außerdem drei Schleifen-Threads, die sich unabhängig voneinander, jeder für sich, mit einem der Namen synchronisieren und dann jeweils ein anderes Sample spielen. Im Endeffekt hören wir jeden halben Schlag einen Klang, da jeder der sync-Threads mit dem cue-Thread auf Zufallsbasis synchronisiert ist und entsprechend sein Sample abspielt.

Das funktioniert natürlich auch, wenn du die Reihenfolge der Threads umkehrst, da die sync-Threads einfach dasitzen und auf den nächsten cue warten.