File: 05.7-Thread-Synchronisation.md

package info (click to toggle)
sonic-pi 3.2.2~repack-8
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 71,872 kB
  • sloc: ruby: 30,548; cpp: 8,490; sh: 957; ansic: 461; erlang: 360; lisp: 141; makefile: 44
file content (137 lines) | stat: -rw-r--r-- 3,668 bytes parent folder | download | duplicates (6)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
5.7 Thread Synchronisation

# Thread Synchronisation

Once you have become sufficiently advanced live coding with a number of
functions and threads simultaneously, you've probably noticed that it's
pretty easy to make a mistake in one of the threads which kills
it. That's no big deal, because you can easily restart the thread by
hitting Run. However, when you restart the thread it is now *out of
time* with the original threads.

## Inherited Time

As we discussed earlier, new threads created with `in_thread` inherit
all of the settings from the parent thread. This includes the current
time. This means that threads are always in time with each other when
started simultaneously.

However, when you start a thread on its own it starts with its own
time which is unlikely to be in sync with any of the other currently
running threads.

## Cue and Sync

Sonic Pi provides a solution to this problem with the functions `cue`
and `sync`.

`cue` allows us to send out heartbeat messages to all other threads. By
default the other threads aren't interested and ignore these heartbeat
messages. However, you can easily register interest with the `sync`
function.

The important thing to be aware of is that `sync` is similar to
`sleep` in that it stops the current thread from doing anything for a
period of time. However, with `sleep` you specify how long you want to
wait while with `sync` you don't know how long you will wait - as
`sync` waits for the next `cue` from another thread which may be soon
or a long time away.

Let's explore this in a little more detail:

```
in_thread do
  loop do
    cue :tick
    sleep 1
  end
end

in_thread do
  loop do
    sync :tick
    sample :drum_heavy_kick
  end
end
```

Here we have two threads - one acting like a metronome, not playing any
sounds but sending out `:tick` heartbeat messages every beat. The
second thread is synchronising on `tick` messages and when it receives
one it inherits the time of the `cue` thread and continues running.

As a result, we will hear the `:drum_heavy_kick` sample exactly when
the other thread sends the `:tick` message, even if the two threads
didn't start their execution at the same time:

```
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
```

That naughty `sleep` call would typically make the second thread out
of phase with the first. However, as we're using `cue` and `sync`, we
automatically sync the threads bypassing any accidental timing
offsets.

## Cue Names  

You are free to use whatever name you'd like for your `cue` messages -
not just `:tick`. You just need to ensure that any other threads are
`sync`ing on the correct name - otherwise they'll be waiting for ever
(or at least until you press the Stop button).

Let's play with a few `cue` names:

```
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
```

Here we have a main `cue` loop which is randomly sending one of the
heartbeat names `:foo`, `:bar` or `:baz`. We then also have three loop
threads syncing on each of those names independently and then playing a
different sample. The net effect is that we hear a sound every 0.5
beats as each of the `sync` threads is randomly synced with the `cue`
thread and plays its sample.

This of course also works if you order the threads in reverse as the
`sync` threads will simply sit and wait for the next `cue`.