Синхронизация потоков

Когда у вас начнёт достаточно хорошо получаться лайвкодинг с большим количеством потоков, выполняющихся одновременно, вы, скорее всего, заметите, что очень легко допустить ошибку, которая может прервать поток. Ничего страшного в этом нет, так как поток может быть перезапущен простым нажатием кнопки “Выполнить”. Однако, после перезапуска потока он будет играть невпопад с другими.

Унаследованное время

Как мы обсуждали ранее, новые потоки, созданные при помощи in_thread наследуют все настройки от родительского потока. Они включают и текущую метку времени. Это означает, что потоки всегда синхронизированы друг с другом после одновременного их запуска.

Но, когда поток стартует отдельно от родительского, внутри него ведётся собственный отсчет времени, который вряд ли совпадёт с каким-либо из других активных потоков.

Функции cue и sync

В Sonic Pi решением этой проблемы являются функции cue и sync.

cue позволяет нам отправлять сигнал пульса другим потокам. По умолчанию, другие потоки не слушают и пропускают эти сообщения. Однако, поток может легко заявить о своей заинтересованности, вызвав функцию sync.

Важно осознавать, что функция sync похожа на sleep, ведь она останавливает текущий поток, и тот некоторое время не выполняется. Для sleep время простоя указывается явно, но вызвав sync, вы не знаете сколько времени оно продлится, потому что sync ждёт следующий cue от другого потока. Это может случиться скоро, а может и нет.

Давайте выясним немного больше деталей:

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

Здесь у нас есть два потока. Один работает как метроном. Он не играет никаких звуков, только отправляет :tick сигнал каждую долю такта. Второй поток синхронизуется с сообщениями tick. Когда он получает :tick сообщение, то наследует временную отметку потока, вызвавшего cue, и продолжает выполнение.

В результате мы будем слышать сэмпл :drum_heavy_kick в тот самый момент, когда другой поток отправляет :tick сигнал. Даже если два этих потока стартовали не одновременно, это всё равно будет происходить:

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

Результатом вызова sleep будет расхождение второго потока с первым. Однако, так как мы используем cue и sync, то мы автоматически синхронизируем потоки и избегаем любых случайных временных сдвигов.

Имена сигналов синхронизации

Для сигналов cue можно использовать любые названия, а не только :tick. Просто следите за тем, чтобы все остальные потоки вызывали sync с правильным именем. Иначе они остановятся навсегда (или, по крайней мере, пока вы не нажмёте кнопку “Остановить”).

Попробуем запрограммировать что-нибудь с использованием нескольких имен cue:

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

Тут у нас есть цикл с функцией cue, отправляющей пульс. Случайным образом этот пульс может быть назван :foo, :bar или :baz. Ещё есть три цикла в потоках, которые синхронизируются с каждым из этих сигналов независимо и воспроизводят разные сэмплы. Чистый эффект от этого кода в том, что каждые полсекунды мы слышим звук, так как каждый из потоков sync случайным образом синхронизируется с потоком cue и проигрывает свой сэмпл.

Конечно же, код будет работать даже если расположить потоки в обратном порядке, поскольку потоки будут дожидаться следующего cue.