sonic-pi
sonic-pi copied to clipboard
Set/get not synced in all threads
I am playing around with the use of set/get. I tried this code: `live_loop :met do; sleep 1; end live_loop :met4 do; sleep 4; end live_loop :met8 do; sleep 8; end live_loop :met16 do; sleep 16; end
live_loop :chordSwitcher, sync: :met do chords = [ [:C4, :major, 4], [:G4, :major, 4], [:A4, :minor, 2], [:G4, :major, 2], [:F4, :major, 4], [:C4, :major, 2], [:G4, :major, 6], ].ring
c = chords[tick]
print("set chord #{c}") set :ch, c set :chl, c[2] set :chc, (chord c[0], c[1]) set :chs, (scale c[0], c[1] == :major ? :harmonic_major : :harmonic_minor)
print("set chord #{c} end") sleep c[2] end
live_loop :bg ,sync: :chordSwitcher do tick print("play chord #{get(:chc)}") play_chord get(:chc), sustain: get(:chl) sleep get(:chl) end
live_loop :mel ,sync: :chordSwitcher do tick print("play mel #{get(:chs)}") use_random_seed 11 + look%8 play (get(:chs).choose) sleep [0.5,1,1,2].choose end`
But unfortunately the melody sometime keeps an old scale instead of switching to the new one.
`{run: 40, time: 21.2, thread: :live_loop_chordSwitcher} ├─ "set chord [:G4, :major, 4]" └─ "set chord [:G4, :major, 4] end"
{run: 40, time: 21.2, thread: :live_loop_mel} ├─ "play mel (ring <SonicPi::Scale :C :harmonic_major [60, 62, 64, 65, 67, 68, 71, 72])" └─ synth :beep, {note: 64.0, release: 0.4}
{run: 40, time: 21.2, thread: :live_loop_bg} ├─ "play chord (ring <SonicPi::Chord :G :major [67, 71, 74])" └─ synth :beep, {note: [67.0, 71.0, 74.0], sustain: 1.6, release: 0.4}`
This problem is reproducable on Linux Mint 20 (Mate 64-bit) with Sonic Pi 3.2.2
The problem is discussed here: https://in-thread.sonic-pi.net/t/set-get-not-in-all-thread/7089/10
I see the same on the latest version (4.1) on Mac. I've simplified the code to this:
live_loop :set do
set :x, tick
sleep 1
end
live_loop :get do
print("loop #{tick}, get: #{get(:x)}")
sleep 1
end
Which gives me this output:
{run: 3, time: 0.0, thread: :live_loop_get}
└─ "loop 0, get: 0"
{run: 3, time: 1.0, thread: :live_loop_get}
└─ "loop 1, get: 0"
{run: 3, time: 2.0, thread: :live_loop_get}
└─ "loop 2, get: 2"
{run: 3, time: 3.0, thread: :live_loop_get}
└─ "loop 3, get: 2"
{run: 3, time: 4.0, thread: :live_loop_get}
└─ "loop 4, get: 3"
{run: 3, time: 5.0, thread: :live_loop_get}
└─ "loop 5, get: 4"
{run: 3, time: 6.0, thread: :live_loop_get}
└─ "loop 6, get: 6"
{run: 3, time: 7.0, thread: :live_loop_get}
└─ "loop 7, get: 7"
It seems non-deterministic whether the :get
loop sees the value set in the :set
loop at the same time or whether it sees the value from the previous loop.
Could this be just a simple race condition? I believe Sonic Pi uses some kind of mutex pattern to synchronise information between threads, but this also means the order of execution is not guaranteed. Even though the set
might start its execution before get
is called, it could be that the data has not been synchronised yet before it's being read. Especially because Sonic Pi syncs up the execution of these live loops, this would be fairly understandable. It also sounds like a hard problem to solve reliably.
As a hack, I have just attempted to run a slightly altered version of @emlyn's example (thanks for that) which does make the execution more reliable on my machine:
live_loop :set do
set :x, tick
sleep 1
end
sleep 0.05
live_loop :get do
print("loop #{tick}, get: #{get(:x)}")
sleep 1
end
Since your chordSwitcher
doesn't actually play any music, it doesn't have to be in perfect sync with the other loops. And, for me, it works:
{run: 15, time: 0.05, thread: :live_loop_get}
└─ "loop 0, get: 0"
{run: 15, time: 0.05}
└─ Timing warning: running slightly behind...
{run: 15, time: 1.05, thread: :live_loop_get}
└─ "loop 1, get: 1"
{run: 15, time: 2.05, thread: :live_loop_get}
└─ "loop 2, get: 2"
{run: 15, time: 3.05, thread: :live_loop_get}
└─ "loop 3, get: 3"
{run: 15, time: 4.05, thread: :live_loop_get}
└─ "loop 4, get: 4"
{run: 15, time: 5.0489, thread: :live_loop_get}
└─ "loop 5, get: 5"
{run: 15, time: 6.0489, thread: :live_loop_get}
└─ "loop 6, get: 6"
{run: 15, time: 7.0489, thread: :live_loop_get}
└─ "loop 7, get: 7"
{run: 15, time: 8.0489, thread: :live_loop_get}
└─ "loop 8, get: 8"
{run: 15, time: 9.0489, thread: :live_loop_get}
└─ "loop 9, get: 9"
{run: 15, time: 10.049, thread: :live_loop_get}
└─ "loop 10, get: 10"
{run: 15, time: 11.049, thread: :live_loop_get}
└─ "loop 11, get: 11"
{run: 15, time: 12.049, thread: :live_loop_get}
└─ "loop 12, get: 12"
It does warn about running behind and I'm sure there's a more architecturally sound decision you could make here, but until someone more knowledgeable replies, I hope this helps a little.
Oh, I thought of an even less appealing option to get rid of the warning and keep the loops in sync:
live_loop :set do
set :x, tick
sleep 1
end
live_loop :get do
sleep 0.125
print("loop #{tick}, get: #{get(:x)}")
sleep 0.875
end
I realise that in a normal language this would be a race condition, but my (maybe incorrect) understanding was that this sort of thing was supposed to be deterministic in Sonic Pi. In fact I think this looks similar to #2934 (maybe it's another manifestation of the same underlying issue).
Considering Sam's response in that thread, I'd say this is expected but not intended behaviour at this time. So yes, that explains it perfectly, thanks.
In a normal language you'd want some sort of main thread to orchestrate the execution order of other threads, but in the case explained in #2934 that would actually mean you'd have to fully execute each thread sequentially. That doesn't sound trivial for this project.
Yep, unfortunately this looks like the issue I mentioned in the other thread. This is definitely something I'm aware of and have plans to address in the future when I re-implement the time-state system in Elixir (hopefully in the next few months).
Many apologies.
Thanks for all your work, there is nothing to apologize. This is a hard edge case, so its no problem, that it is a problem :)
To enter some more non working codeblocks: I tried to fix the problem with a time_warp, but this doesn't work. I changed the example from @emlyn to
live_loop :set do
time_warp -0.02 do
set :x, tick
print("set to #{look}")
end
sleep 1
end
live_loop :get do
print("loop #{tick}, get: #{get(:x)}")
sleep 1
end
Result:
{run: 20, time: -0.02, thread: :live_loop_set}
└─ "set to 0"
{run: 20, time: 0.0, thread: :live_loop_get}
└─ "loop 0, get: 0"
{run: 20, time: 0.98, thread: :live_loop_set}
└─ "set to 1"
{run: 20, time: 1.0, thread: :live_loop_get}
└─ "loop 1, get: 0"
{run: 20, time: 1.98, thread: :live_loop_set}
└─ "set to 2"
{run: 20, time: 2.0, thread: :live_loop_get}
└─ "loop 2, get: 2"
{run: 20, time: 2.98, thread: :live_loop_set}
└─ "set to 3"
{run: 20, time: 3.0, thread: :live_loop_get}
└─ "loop 3, get: 2"
{run: 20, time: 3.98, thread: :live_loop_set}
└─ "set to 4"
{run: 20, time: 4.0, thread: :live_loop_get}
└─ "loop 4, get: 4"