sonic-pi icon indicating copy to clipboard operation
sonic-pi copied to clipboard

Set/get not synced in all threads

Open synchronisator opened this issue 1 year ago • 7 comments

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

synchronisator avatar Sep 10 '22 13:09 synchronisator

The problem is discussed here: https://in-thread.sonic-pi.net/t/set-get-not-in-all-thread/7089/10

synchronisator avatar Sep 10 '22 13:09 synchronisator

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.

emlyn avatar Sep 10 '22 22:09 emlyn

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

jhaagmans avatar Sep 11 '22 09:09 jhaagmans

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).

emlyn avatar Sep 11 '22 14:09 emlyn

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.

jhaagmans avatar Sep 11 '22 15:09 jhaagmans

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.

samaaron avatar Sep 11 '22 16:09 samaaron

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"

synchronisator avatar Sep 11 '22 18:09 synchronisator