Time state get and set issue

I found the sample code in the tutorial highly dependent on the order of live_loop defined, if I switch the order of those two live_loops (in the tutorial it defines the shuffle ahead of sorted), as such below, the result would be again in a chaos like without using time state.

## An Example of Deterministic Behaviour
## (despite concurrent access of shared state)
## using Sonic Pi's new Time State system.
##
## When this code is executed, the list that's
## printed is always sorted!
set :a, (ring 6, 5, 4, 3, 2, 1)

live_loop :sorted do
  set :a, get[:a].sort
  sleep 0.5
  puts "sorted: ", get[:a]
end

live_loop :shuffled do
  set :a, get[:a].shuffle
  sleep 0.5
end

the output is:

=> Starting run 11

=> Redefining fn :live_loop_sorted

=> Redefining fn :live_loop_shuffled

{run: 11, time: 0.5, thread: :live_loop_sorted}
 └─ "sorted: " (ring 4, 6, 2, 3, 1, 5)
 
{run: 11, time: 1.0, thread: :live_loop_sorted}
 └─ "sorted: " (ring 6, 2, 3, 5, 1, 4)
 
{run: 11, time: 1.5, thread: :live_loop_sorted}
 └─ "sorted: " (ring 1, 3, 5, 6, 2, 4)
 
{run: 11, time: 2.0002, thread: :live_loop_sorted}
 └─ "sorted: " (ring 3, 2, 4, 1, 6, 5)
 
=> Stopping all runs...

=> Stopping run 11

{run: 11, time: 2.5002, thread: :live_loop_sorted}
 └─ "sorted: " (ring 1, 4, 6, 5, 2, 3)
 
{run: 11, time: 3.0002, thread: :live_loop_sorted}
 └─ "sorted: " (ring 6, 5, 4, 1, 2, 3)
 
=> Completed run 11

=> All runs completed

=> Pausing SuperCollider Audio Server


I believe the key reason is rather happening in the sleep 0.5, when shuffle may take part in no matter whether you use getter and setter or not.
But the problem is if I simply change the order of the loops defined, like this

## An Example of Deterministic Behaviour
## (despite concurrent access of shared state)
## using Sonic Pi's new Time State system.
##
## When this code is executed, the list that's
## printed is always sorted!
set :a, (ring 6, 5, 4, 3, 2, 1)

live_loop :shuffled do
  set :a, get[:a].shuffle
  sleep 0.5
end

live_loop :sorted do
  set :a, get[:a].sort
  sleep 0.5
  puts "sorted: ", get[:a]
end

then the output is changed as well, that means the result can’t be controlled

=> Starting run 17

=> Defining fn :live_loop_shuffled

=> Redefining fn :live_loop_sorted

{run: 17, time: 0.5001, thread: :live_loop_sorted}
 └─ "sorted: " (ring 1, 2, 3, 4, 5, 6)
 
{run: 17, time: 1.0, thread: :live_loop_sorted}
 └─ "sorted: " (ring 1, 2, 3, 4, 5, 6)
 
{run: 17, time: 1.5, thread: :live_loop_sorted}
 └─ "sorted: " (ring 1, 2, 3, 4, 5, 6)
 
=> Stopping all runs...

=> Stopping run 17

{run: 17, time: 2.0, thread: :live_loop_sorted}
 └─ "sorted: " (ring 1, 2, 3, 4, 5, 6)
 
{run: 17, time: 2.5, thread: :live_loop_sorted}
 └─ "sorted: " (ring 1, 2, 3, 4, 5, 6)
 
=> Completed run 17

=> All runs completed

=> Pausing SuperCollider Audio Server

Hello! Thanks for asking.

I’m not entirely sure about your question, but the behavior you’re seeing is actually completely deterministic. Try the following:

set :a, (ring 6, 5, 4, 3, 2, 1)

live_loop :sorted do
  set :a, get[:a].sort
  puts "sorted: ", get[:a]
  sleep 0.5
  puts "sorted: ", get[:a]
end

live_loop :inverse do
  set :a, get[:a].sort.reverse
  sleep 0.5
end

I’m assuming you’re seeing that misbehavior because puts is getting the state once it’s modified, but otherwise everything seems as expected.

let me know if I made myself clear :thinking:

Hi, thank you for the reply. I have tried with your code and here is the output, it is indeterministic:

=> Starting run 15

=> Defining fn :live_loop_sorted

=> Defining fn :live_loop_inverse

{run: 15, time: 0.0, thread: :live_loop_sorted}
 └─ "sorted: " (ring 1, 2, 3, 4, 5, 6)
 
{run: 15, time: 0.5, thread: :live_loop_sorted}
 ├─ "sorted: " (ring 6, 5, 4, 3, 2, 1)
 └─ "sorted: " (ring 1, 2, 3, 4, 5, 6)
 
{run: 15, time: 1.0, thread: :live_loop_sorted}
 ├─ "sorted: " (ring 6, 5, 4, 3, 2, 1)
 └─ "sorted: " (ring 1, 2, 3, 4, 5, 6)
 
{run: 15, time: 1.5, thread: :live_loop_sorted}
 ├─ "sorted: " (ring 6, 5, 4, 3, 2, 1)
 └─ "sorted: " (ring 1, 2, 3, 4, 5, 6)
 
{run: 15, time: 2.0001, thread: :live_loop_sorted}
 ├─ "sorted: " (ring 6, 5, 4, 3, 2, 1)
 └─ "sorted: " (ring 1, 2, 3, 4, 5, 6)
 
{run: 15, time: 2.5001, thread: :live_loop_sorted}
 ├─ "sorted: " (ring 6, 5, 4, 3, 2, 1)
 └─ "sorted: " (ring 1, 2, 3, 4, 5, 6)
 
{run: 15, time: 3.0001, thread: :live_loop_sorted}
 ├─ "sorted: " (ring 6, 5, 4, 3, 2, 1)
 └─ "sorted: " (ring 1, 2, 3, 4, 5, 6)
 
{run: 15, time: 3.5001, thread: :live_loop_sorted}
 ├─ "sorted: " (ring 6, 5, 4, 3, 2, 1)
 └─ "sorted: " (ring 1, 2, 3, 4, 5, 6)
 
{run: 15, time: 4.0001, thread: :live_loop_sorted}
 ├─ "sorted: " (ring 6, 5, 4, 3, 2, 1)
 └─ "sorted: " (ring 1, 2, 3, 4, 5, 6)
 
{run: 15, time: 4.5001, thread: :live_loop_sorted}
 ├─ "sorted: " (ring 6, 5, 4, 3, 2, 1)
 └─ "sorted: " (ring 1, 2, 3, 4, 5, 6)
 
{run: 15, time: 5.0001, thread: :live_loop_sorted}
 ├─ "sorted: " (ring 6, 5, 4, 3, 2, 1)
 └─ "sorted: " (ring 1, 2, 3, 4, 5, 6)
 
=> Stopping all runs...

=> Stopping run 15

{run: 15, time: 5.5001, thread: :live_loop_sorted}
 ├─ "sorted: " (ring 6, 5, 4, 3, 2, 1)
 └─ "sorted: " (ring 1, 2, 3, 4, 5, 6)
 
=> Completed run 15

=> All runs completed

=> Pausing SuperCollider Audio Server

please understand why I’m saying so, because if I just define the two loops in another order, I will have different output,

set :a, (ring 6, 5, 4, 3, 2, 1)

live_loop :inverse do
  set :a, get[:a].sort.reverse
  sleep 0.5
end

live_loop :sorted do
  set :a, get[:a].sort
  puts "sorted: ", get[:a]
  sleep 0.5
  puts "sorted: ", get[:a]
end
=> Starting run 16

=> Redefining fn :live_loop_inverse

=> Redefining fn :live_loop_sorted

{run: 16, time: 0.0, thread: :live_loop_sorted}
 └─ "sorted: " (ring 1, 2, 3, 4, 5, 6)
 
{run: 16, time: 0.4999, thread: :live_loop_sorted}
 ├─ "sorted: " (ring 1, 2, 3, 4, 5, 6)
 └─ "sorted: " (ring 1, 2, 3, 4, 5, 6)
 
{run: 16, time: 1.0002, thread: :live_loop_sorted}
 ├─ "sorted: " (ring 1, 2, 3, 4, 5, 6)
 └─ "sorted: " (ring 1, 2, 3, 4, 5, 6)
 
{run: 16, time: 1.5002, thread: :live_loop_sorted}
 ├─ "sorted: " (ring 1, 2, 3, 4, 5, 6)
 └─ "sorted: " (ring 1, 2, 3, 4, 5, 6)
 
{run: 16, time: 2.0002, thread: :live_loop_sorted}
 ├─ "sorted: " (ring 1, 2, 3, 4, 5, 6)
 └─ "sorted: " (ring 1, 2, 3, 4, 5, 6)
 
=> Stopping all runs...

=> Stopping run 16

{run: 16, time: 2.5002, thread: :live_loop_sorted}
 ├─ "sorted: " (ring 1, 2, 3, 4, 5, 6)
 └─ "sorted: " (ring 1, 2, 3, 4, 5, 6)
 
=> Completed run 16

=> All runs completed

=> Pausing SuperCollider Audio Server

:thinking:, lets forget about the puts. if I use it with sound I hear same sound pattern no matter where I put the order of the loops

use_random_seed 1

set :a, (ring 20, 40, 60, 80, 90)


live_loop :sorted do
  use_synth :dsaw
  set :a, get[:a].sort
  play get[:a].tick
  sleep 2
end

live_loop :inverse do
  set :a, get[:a].sort.reverse
  play get[:a].tick
  sleep 2
end

well, actually no. You changed the sleep position. Try this code and change the order of definition of loops, you will hear different things.

use_random_seed 1

set :a, (ring 20, 40, 60, 80, 90)


live_loop :sorted do
  use_synth :dsaw
  set :a, get[:a].sort
  sleep 2
  play get[:a].tick
end

live_loop :inverse do
  set :a, get[:a].sort.reverse
  play get[:a].tick
  sleep 2
end

yes it sounds different from the one I send and makes sense, cause Is sorted sleep 2 acts first, but If I grab the code you send, and I press play I am hearing the same pattern always.

maybe I am missing something :thinking:

The individual patterns played by each loop do not change between runs, so in that respect, you’ll hear the same patterns being played over time. However, the order in which the two loops are defined determines the patterns that are played. If I understand correctly I think what nordicwing is getting at is that they believe it should not do so…

No, you are right, that’s also what I’m thinking, during the sleep things are changed. But still, just by changing the position of definition we get indeterministic result. And also, the way of the code I sent you is from the tutorial, it’s like a official guide of practice, but this way of coding can’t guarantee the result.

Yes you are right, that’s what I’m concerned. Also the code is actually from the tutorial,

## An Example of Deterministic Behaviour
## (despite concurrent access of shared state)
## using Sonic Pi's new Time State system.
##
## When this code is executed, the list that's
## printed is always sorted!
set :a, (ring 6, 5, 4, 3, 2, 1)

live_loop :shuffled do
  set :a, get[:a].shuffle
  sleep 0.5
end

live_loop :sorted do
  set :a, get[:a].sort
  sleep 0.5
  puts "sorted: ", get[:a]
end 

If you swap the order of the two definitions, it outputs the different results – just as the indeterministic result without using time state shown by the example previously.

The guiding principle of Sonic Pi’s determinism is that the same code should produce the same results.

The important thing to note is that A, B, C is not the same as B, A, C - and this counts for the ordering of Sonic Pi’s Live Loops too. Order matters and is key to solving race conditions deterministically.

If you say “the two Live Loops are concurrent, they surly can be switched in place and have the same result” then you need to have a good answer as to how race conditions between the two threads are solved deterministically. This is especially true in very esoteric cases like your example where you’re explicitly trying to get the two threads to modify the same state at the same time! It’s almost like you’re looking for the edge cases!

On a practical level, couldn’t you just make two local copies of the array inside each loop, and tick through those? That way the data each loop was traversing would be encapsulated in the loop and immune to weird side effects. This just seems like good programming hygiene.

## An Example of Deterministic Behaviour
## (despite concurrent access of shared state)
## using Sonic Pi's new Time State system.
##
## When this code is executed, the list that's
## printed is always sorted!
set :a, (ring 6, 5, 4, 3, 2, 1)

live_loop :shuffled do
  shuffleloop = get[:a].shuffle
  sleep 0.5
end

live_loop :sorted do
  sortloop =  get[:a].sort
  sleep 0.5
  puts "sorted: ", sortloop
end