Beware of Routine.stop.reset

You’d think it should work but it doesn’t to chain them like that.

The reason is somewhat obscure but probably related to the interpreter global lock needing to be released.

x = Routine { loop { "Hi".postln; 1.wait } }
x.play

x.stop.reset // keeps going!!

x.stop // this stops it
x.reset // ok like this, won't restart it, by the way

The global interpreter lock is actually the cause of very few problems, and it isn’t the cause of this behavior.

If you schedule the routine for 1000 seconds into the future, and .stop.reset it, certainly sometime in the intervening 16 minutes and 40 seconds the interpreter lock would be released (at least I hope so – otherwise your CPU would melt), but you would still see the noted behavior.

The problem occurs because you cannot remove a scheduled item after it’s been scheduled. The only way to prevent a scheduled activity from happening is to stop the clock (supported for TempoClock, not for SystemClock or AppClock). That would kill every activity scheduled on that clock. Usually you don’t want that, so the nuclear option is typically not preferred.

SC’s solution is to wake up the item as scheduled, but if the item is in a stopped state, to do nothing – including, don’t reschedule for later. It’s the lack of rescheduling that truly causes the task to become idle. It isn’t fully idle until that point.

If you stop and immediately reset, then the routine is first put into “stopped” state – at which point, to stop completely, it should be allowed to awake. But the immediate reset overwrites the “stopped” state and makes the routine ready to run on the next awake. So it continues.

A good case could be made that some complexity could be eliminated by implementing methods/primitives to remove items from clock queues. A better way to make the case would be for someone to actually write the primitives and put in a PR.

hjh

You’re correct. If I increase the rescheduling interval to, say,. 6 seconds

x = Routine { arg time; loop { postln("Hi" + time); time = 6.wait } }

x.play

Then I have enough time to type (or ctrl+enter) both commands in the interval between activations

x.stop // hit right after a "Hi" message is posted
x.reset // and this one soon thereafter

So I get the same behavior from that as with x.stop.reset, meaning the stop gets “eaten” by the reset.

So, after mulling over (on github) the plausible solutions on making this .stop.reset just work as-is , they are all pretty heavy weight, so not realistic for SC3, and probably even objectionable as they would go against the simplicity of the current state machine for thread/routine states.

As a manual workaround, this pattern was useful to me: don’t reuse a Routine by resetting it if it could go on clocks, even indirectly, e.g. via Conditions. Instead create a new Routine that wraps the same function and just stop the old Routine.

Instead of

r = Routine ({ loop { "Hi".postln; 1.wait } })
r.play
/// ...
r.stop.reset

write

f = { loop { "Hi".postln; 1.wait } };
r = Routine(f);
r.play;
/// ...
r.stop;
r = Routine(f);

I suspect there may be more elegant solutions, but the above works with the basic SC machinery.