Deep dive into threading, blocking, sync/async, scheduler internals (split from scztt quarks thread)

I wasn’t clear, my point was that a Stream is not a Routine (nor a Thread) in OOP. Stream defines next and reset, Routine defines awake[1], play and stop, so it could define pause and resume.

[1] Yes, awake is defined in object, for the same reason nil knows everything, but in practice only works with routines and functions.

It works with “pure” streams too, like FuncStream, although arguably that’s only a wrapper for a pair of functions. Eventually any fancy thing that executes needs a function somewhere in its guts :smiley:

By the way, I think awake should work for AbstractFunction as well, meaning the override that it gets in Function should really be in the superclass AbstractFunction. I suspect this may have been an oversight because PauseStream overrides awake with the exact same implementation that it gets from Object anyway. Since PauseStream is a sub-class of AbstractFunction, but not of Function, I suspect the re-override was put in PauseStream because someone thought that AbstractFunction overrides awake from Object, although it doesn’t actually do that.

I’ll answer James later. I found a little issue with FlowVar in the meantime. It doesn’t affect the above example, but it does affect my solution to that.

Yep, but I don’t think that is the intended use, the documentation only talk about routines and functions, but since funcstream is an abstractfunction, and, well etc., you know.

@jamshark70 PauseStream doesn’t touch the clock’s scheduling queue (AFAI can see), it adds an intricate pause state which makes next method to return nil (and calculates the delta from the pause call, something that I forgot somewhere else).

Which documentation? It wasn’t documented for function as far as I can tell.

It doesn’t touch the clocks queues but it does touch the Conditions’ queues. It does a substitution there, basically, at addition time. It’s best to look at the commit that added it to see all the interrelated bits.

There’s no need to do anything special with the clock queues because one normally adds the (PauseStream) wrapper to those. So those queues will easily get the right object, unlike the Condition queues which needed the threadPlayer hack.

Somewhere in the docs related with scheduling, I think, can’t find it now. But the thing for me is in which cases can/may be really used? Because everything is a stream and everything can be scheduled, but why? It allows nonsensical cases:

SystemClock.sched(0, { "evaluated once".postln; });  // A way of evaluating a function.
SystemClock.sched(0, { ":D".postln; 1; });  // You can't stop me.
SystemClock.sched(0, "string");  // If a string is evaluated in a clock and no one is around to hear it, does it make a sound?
SystemClock.sched(0, nil);  // Consistent.
SystemClock.sched(0, 1);  // A second running in the background.
SystemClock.sched(0, 0);  // will hang the language.

By the way, the “addition time” bit turns out to be pretty limiting, as I discovered. It’s because the wrapper must exist and have already wrapped the target routine before the latter does any Condiion.wait. But except for simple uses like Task and EventSteamPlayer which spawn their own-ed Routines in their own constructor, in my case the Routines exist before / independently. So they don’t yet have any threadPlayer wrapper set when they wait a Condition… Bassically I have to fake a sort of proxy-Routine that does nothing but is created and then gets filled with stuff that does the work, just so the ref to it can be first pasted into the sub-Routines’ threadPlayer. Like a a forward declaration of sorts.

Alternatively, it would have made more sense for threadPlayer to be a Ref to a Routine (or Stream), so that it can be changed after it’s already on the Condition queues. But that would require patching quite a few places since Refs aren’t exactly transparent in SC (you have to call .value on them).

Sorry, I can’t see a condition being used in PauseStream-pause logic, I see it sets stream to nil so if called after delta and before resume it will get out of the clock and then sets stream again and nextBeat to nil so it will not be played before last delta.

It matters if the wrapped stream uses a Condition itself. Because, simplifying the code slightly

Condition {
	signal {
		//// .....
			waitingThreads.do({ arg thread;
				thread.clock.sched(0, thread);
			});
	}
}

So it moves the waiting threads to the clock’s queue. The question is which one(s): the wrapper or the wrapped. In order to move the wrapper, what gets added to the Cond’s queue is:

	hang { arg value = \hang;
		waitingThreads = waitingThreads.add(thisThread.threadPlayer);
		value.yield;
	}

So it’s not always the calling Thread that gets added to the queue but sometimes its wrapper which is pointed to by threadPlayer. And PauseStream sets itself as the wrapper aka threadPlayer. A rather misleading name was chosen for this field, by the way. I guess it makes sense if you only consired EventStreamPlayers as the only wrappers, 'cause they have “Player” in name, but PauseStream, which is the actual class where the wrapper functionality is implemented, ain’t called a “player”. Duh. Also, one can set the threadPlayer (of a Routine) to any other Routine or anything that supports being scheduled by the clock really, i.e. something that has a working awake.

Epic. I had realized that if you schedule a number it will keep on running (completely silently) because it (a) has awake due to Object and (b) returns itself on next. Also works for a Ref by the way, which is a bit more interesting perhaps, cause you can change what it points to.

AppClock.play(x = Ref(1))
x.value = 0 // high CPU load; doesn't crash for me!
x.value = 2 // relax

Maybe the sys clock is more crash prone. Afraid to try :stuck_out_tongue: Actually I did try it. It seems that
SystemClock has a problem in that it goes to 100% CPU on x.value = 0 but it doesn’t recover when you set x.value = 2, so live-locks the interpreter.

Also a FuncStream is slightly more useful than a plain Function here because you can change the wrapped nextFunc while it runs, without even resorting to jitlib stuff.

f = FuncStream { 1 }
AppClock.play(f)
f.nextFunc = { "Hi!!!".postln; 0.2 }
f.nextFunc = { "Stop".postln }

FuncStream basically works like a FunctionRef here.

I already gave an example like this further above, but if you want it (externally) pause-able, i.e. not implement some logic yourself, and don’t want to use a Routine for some (performance?) reason, as Task would make one for you:

p = PauseStream(f = FuncStream {1}, AppClock).play
f.nextFunc = { "Hi!!!".postln; 0.2 }
p.pause // stops it
p.resume // resumes it

Probalby the quickest way to check that it’s running on the main thread is to try yield something from it.

f.nextFunc = { 5.yield } // ERROR: yield was called outside of a Routine.

I thought for a while I knew how to showcase the necessity of that threadPlayer field, but it turns out that I really couldn’t. Maybe Julian can share his private test case for that.

Actually, I got tricked by some simple threadPlayer resetting behavior in PauseStream. Here’s the hack around it, to showcase what nixing threadPlayer does.

(
var fv = FlowVar();
p = PauseStream(r = Routine { loop {
	("Got" + fv.value).postln; 1.yield }}, AppClock).play;
~fv = fv;
)

p.pause // will (normally) prevent the next line
// from immediately awaking the inner Routine

~fv.value = 12
p.resume // needed to see effect

// do CmdPeriod here

But, nixing the threadPlayer in the following routine makes pause ineffective

(
var fv = FlowVar();
p = PauseStream(r = Routine { loop {
	thisThread.threadPlayer = thisThread; // "freedom"!
	("Got" + fv.value).postln; 1.yield }}, AppClock).play;
~fv = fv;
)

p.pause // not effective now!

~fv.value = 12
// -> a FlowVar
// Got 12
// Got 12

The other obscure thing that bit me is that this in that Routine is actually Interpreter; only thisThread points to the Routine itself.

The reason you still see the r = there and why that’s not just a Task is that I tried to change the threadPlayer externally via that r, but PauseStream is good at resetting it back just in the nick of time, before calling the actual Routine. Only by making the change in the routine itself, right before the Condition inside the FlowVar gets wait-ed, was I able to get past PauseStream’s defenses.

this is the interpreter, for all code outside of class definitions. It’s always been such.

hjh

I actually tried to use just the threadPlayer bit to implement Unix-style select fd semantics over SC streams. It turned out it’s impossible to do it just with that threadPlayer trick. The reason for this impossibility is that threadPlayer lets you substitute one thread for another in the scheduler queues, but for a unix-semantics select you need to co-schedule two threads, the producer and the [select-]consumer. And that one can’t do just with the threadPlayer when there are multiple producers involved. For PauseStream, there is a single producer, namely the stream being wrapped, so the consumer (PauseStream) may decide based solely on the internal state of the consumer (paused state or not) whether to run the producer or not. But in a Unix-select style situation, there are multiple producers, and so the consumer stealing the scheduler activation for one (or even all) of them doesn’t alone help it know which of these producers should have ran so as to run it itself as PauseStream does. So, the threadPlayer bit is not terribly useful outside the PauseStream business.

@jamshark70 To answer your earlier question as what was this for, I was trying to do “reactive streams”, where they don’t merely spit out data for a score-gen, but can block waiting for “external events”–which can be just something else in the program, not necessarily truly external, like MIDI. Alas, something like that’s not easy to pull off with SC’s conception of streams. You need additional synchronization objects which ultimately make the streams pointless as you can just use arrays, queues etc. together with synchronization.

Ok, now it all makes more sense. Tbh I was totally baffled by the obsessiveness of recent activity. It would have helped me tremendously to understand where you’re coming from if you had simply said this is what you were after, from the start. Unless you did say and I missed it (there’s also a problem with flooding the forum with a dozen lengthy posts a day, for days on end – diluted content dilutes attention). I had hoped I could fill in with some background information but after awhile, I no longer knew how to respond to pretty much any of it.

Agreed that SC coroutines and the current scheduling model by themselves do not work well for e.g. the type of producer-consumer threading you mentioned.

I tend to think of SC classes as ingredients to be combined into a recipe, rather than a box of ready-to-eat takeout. Nobody has created classes for reactive streams yet but that doesn’t mean it’s impossible to do so. Attempting to force Routines to be such is likely to fail, but a superstructure using Routines may likely succeed. (Despite PauseStream being criticized earlier, I still believe it’s a good approach because the wrapper methodology can be applied to an essentially unlimited variety of problems, whereas nobody wants to write more threading primitives for every new use case.)

hjh

It’s in the clocks documentation. E.g. AppClock or SystemClock help only show the item scheduled being a function or Routine but also name-drop Task as being schedulable. AppClock’s says:

In general, I found the documentation for the clock pages among the most confusingly written. E.g. the one from TempoClock (mostly) uses “task” to refer to what the one for AppClock usually calls an “item” etc.

TempoClock’s page has expressions or explanations like

.schedAbs(beat, item)
Schedules a task to be performed at a particular time in beats.

.play(task, quant: 1)
Plays task (a function) at the next beat, where quant is 1 by default.

Whereas:

AppClock.play(task)
The Routine (or Task) yields a float value indicating the delta (secs) for the AppClock to wait until resuming the Routine.

Task is a PauseStream.

There’s no detail on two of the three clock pages how the sausage is made, i.e. no mention of awake being the important bit on the “item” or “task”. I guess that was done in an attempt to make it newbie friendly, but apparently that has resulted in some hardened convictions in the minds of some that that is some kind of specification for what’s schedulable.

The help for TempoClock does say eventually, under schedAbs that:

When the scheduling time is up, the task’s awake method is called. If the method returns a number, the task will be rescheduled for the time equal to the last scheduling time plus the returned value.

There’s actually a more useful overview in the Clock superclass help, but since that’s not a class used directly, I guess nobody reads that page. But it says:

Objects of different classes may do different things in response to being scheduled on a clock by having own implementation of the awake method. The Object: -awake method that all classes inherit simply calls the same object’s next method, forwarding the beats argument as well as the return value, so subclasses may implement either one to equivalent effect, as far as clock scheduling is concerned.

Examples of useful objects to be scheduled on clocks:

  • Function: -awake method is implemented so as to call the function’s own -value method, effectively running the code within the function.
  • Routine: -awake calls own Routine: -next, in turn starting or resuming the Routine’s Function.
  • Some subclasses of Stream will have its next method do something useful aside from returning a new value in a stream.

But it gives no concrete examples of the latter; the Task class is no mentioned there, only generic “tasks”…

So it is said in the help that (some) Streams can also usefully go on clocks, albeit it was a bit hard to find the place where that’s said.

As for:

You cannot actually schedule AbstractFunctions. Their awake is broken by not redirecting to their value and next for AbstractFunctions just returns the AbstractFunction, not its value.

For FuncStream and Streams awake works because it has a next that calls the wrapped nextFunc function.

{ 1 + 2 }.awake // 3
{ 1 + 2 }.next  // -> a Function

({ 1 } + 2).awake // -> a BinaryOpFunction
({ 1 } + 2).value // 3 
({ 1 } + 2).next  // -> a BinaryOpFunction

(FuncStream { 1 + 2 }).awake // 3
(FuncStream { 1 + 2 }).next  // 3
(FuncStream { 1 + 2 }).value // 3 (as well)

What you observe regarding naming happens in other classes too, specially with parameters names or missing documentation. Maybe because undocumented methods, such as awake, are internal methods. IIRC the only call to awake is done by clocks at low level magic in C/C++. It also happens that polymorphism was designed to happen at Object/Nil (root class) level for many methods and that makes it difficult to know where those methods can be correctly applied.

That creates some issues IMO. There have been many decisions over the years arguing “this method for this class is not working, it should work like this…”, and whether accepted or not, useful or not, it doesn’t matter, they are arbitrary decisions that appeal to the “common sense” of sclang, which varies over the time.

There’s no detail on two of the three clock pages how the sausage is made, i.e. no mention of awake being the important bit on the “item” or “task”. I guess that was done in an attempt to make it newbie friendly, but apparently that has resulted in some hardened convictions in the minds of some that that is some kind of specification for what’s schedulable.

I don’t get the meaning of “hardened convictions” adjectivation, doesn’t sound good, it’s like enumerating someone :D. Because the sausage recipe is not explicit, in fact it is explicit but ambiguous, some people may think it can be used for things that will not work, in many cases in contradiction with other parts of the library. Some people may think that is the openness of possibilities, like a living language, but if at the end no one knows what we are talking about the whole thing makes no sense, it becomes a technical issue theoretically speaking.

Kind of PS. As another example, in the documentation of Thread it says that they inherit the associated clock of the parent but it doesn’t explain what that means and how it behaves and somehow we have to make sense of it. Things I’ve thought were the most logical were not, they were things that occurred to me should behave in some ways under some circumstances, turns out it was designed by someone else, meaning something else, for a particular purpose. Then I realize that things aren’t what I think they should be but what someone else decided. So I changed the language for me, check mate (it’s a joke :D).

I was myself mislead by the documentation on more than one occasion, see e.g. Task-Condition issue, so when I wrote that something implied by omission in some help page resulted in “hardened convictions” I did not mean to assert the fault lies with the person who read the said documentation…

Probably a good approach would be for the various clocks help pages to refer the reader more explicitly to superclass Clock page for the overview of what can be sensibly scheduled on a clock. Well, ok, those sub-pages do say:

See Clock for general explanation of how clocks operate.

The varying terms used in those sub-pages like “task”, “item” etc. could probably deserve some reconsideration as to whether a single one should be employed uniformly or at least some notice be given ahead that that or those are generic terms whose instancing is explained in the main Clock page.

After mulling this over, I agree with you (and James Harkins) now that there’s
duck-type-based hidden class in the hierarchy that splits streams into
those that can usefully go on a clock and those that can’t because they
don’t conform to the clock protocol, which is embedded in the operating
assumptions of some sclang synchronization primitives, like CondVar or Condition.)

So the real hierarchy in sclang, taking that into account is at least something like

Stream--[ClockStream]--PauseStream(wraps a ClockStream arg)

PauseStream isn’t exactly useful at wrapping a “pure” stream unless that wrapped stream is of this hidden ClockStream class. So when I wrote (a whole bunch of posts before) this example, which I’m slightly simplifying here

f = FuncStream { "Hi!!!".postln; 0.2 }
p = PauseStream(f, AppClock).play
p.pause // stops it
p.resume // resumes it

It works because FuncStream { "Hi!!!".postln; 0.2 } is actually of this
duck-typed, undeclared ClockStream class.

I also agree now that the bugs that I had reported against FlowVar, Condition, and CondVar aren’t really so (so I closed them) because it is indeed expected, although alas not explicitly documented, that these classes get run from a Routine that only runs on a clock, not just any kind of Routine. So, really the hierarchy is more like

Stream--[ClockStream]--PauseStream
 \            \ 
  \            \ 
   \---Routine--[ClockOnlyRoutine]

While one can write “mixed” routines that can even tell the difference when they run a clock and when they don’t, that approach isn’t really supported by most parts of the class library. It wasn’t exactly obvious to me though that that must necessarily be the case.

And to add to the sclang gotchas repertoire, EventStreamPlayer is a bit more demanding of
the ClockStream multiplexed into dur and in the underlying delta too, which seems to the lowest-level field that clock timing translates to in the Event business), namely it won’t accept a self-pausing dur or even such a delta stream, i.e. one with symbols in it.

Pbind(\delta, Pseq([1, 2, \hang, 1])).play
// ^^ The preceding error dump is for ERROR: Primitive '_Event_Delta' failed. Wrong type.

So additionally:

Stream--[ClockStream]--[NonSelfPausingClockStream == NumberStream]

And EventStreamPlayer only accepts the last kind for its dur or even delta sub-stream.


By the way, I’ve recently realized you’re the main dev of the Python sc3 package, so that explains why you’re not reading sclang help much nowadays :slight_smile:

There’s actually a moderately tricky way to handle that: Put the sync into an event type.

// First demonstrate the problem

s.boot;

(
SynthDef(\buf1, { |out, gate = 1, bufnum, rate = 1, start = 0, amp = 0.1|
	var sig = PlayBuf.ar(1, bufnum, rate, startPos: start);
	var eg = EnvGen.kr(Env.asr(0.01, 1, 0.01), gate, doneAction: 2);
	Out.ar(out, (sig * eg * amp).dup);
}).add;
)

// This version of the pattern is unsynced.
// The buffer isn't ready for the first note event,
// so it posts "Buffer UGen: no buffer data" at start
// (The same message appears at the end,
// but that's premature cleanup and I'm not addressing that issue here)
(
p = Pproto(
	{
		~bufnum = (type: \allocRead,
			path: Platform.resourceDir +/+ "sounds/a11wlk01.wav"
		).yield;
	},
	Pbind(
		\instrument, \buf1,
		\start, Pwhite(0, 80000, inf),
		\dur, Pexprand(0.1, 0.5, inf),
		\legato, 0.99
	)
).play;
)

p.stop;


// now let's add a syncing event type and dummy cleanup
(
Event.addEventType(\sync, {
	~server.sync;
	// 0.yield;  EDIT: I thought at first this is needed, but it's not, can delete safely
});

EventTypesWithCleanup.cleanupTypes.put(\sync, \rest);
)

// and... the init message goes away

s.dumpOSC(1);

(
p = Pproto(
	{
		~bufnum = (type: \allocRead,
			path: Platform.resourceDir +/+ "sounds/a11wlk01.wav"
		).yield;
		(type: \sync).yield;
	},
	Pbind(
		\instrument, \buf1,
		\start, Pwhite(0, 80000, inf),
		\dur, Pexprand(0.1, 0.5, inf),
		\legato, 0.99
	)
).play;
)

[ "#bundle", 16535965446379165886, 
  [ "/b_allocRead", 0, "/usr/local/share/SuperCollider/sounds/a11wlk01.wav", 0, 0 ]
]
[ "#bundle", 1, 
  [ "/sync", 1009 ]  <<-- there!
]
[ "#bundle", 16535965446535959182, 
  [ 9, "buf1", 1053, 0, 1, "out", 0, "bufnum", 0, "start", 37018, "amp", 0.1 ]
] ... etc.

p.stop;
s.dumpOSC(0);

I feel as though that advice had been seen initially as a hindrance, or unnecessary roadblock, and met with resistance – I hope now it can be understood as an attempt to steer you toward uses of streams that are known to be stable. No harm was intended – but, from my side, those conversations were unnecessarily stressful, and that’s leading me to reevaluate which threads I’d be willing to get involved in vs threads that I won’t. (Well, not only that – it’s dawning on me that being highly active on the forum has changed my relationship with SC away from “creative tool” and toward “problem-solving environment” – which is fun in its way, but for me, 2022 needs to be about creativity rather than technology. So I’ll be making efforts to post less here – I’ll probably take a week off from posting, from this point, and refocus on audio.)

In any case, I think it’s not a bad outcome that you created your own sync classes. Changing core behavior always involves some risk. IMO it’s better to prototype new use cases separately from the main class library (where we are now), and then later figure out which of the new ideas might be incorporated.

hjh

I apologize for my contribution to that stress. I certainly appreciate all the insightful and timely answers you provide here and on github.

^^ I appreciate that, thank you.

There’s almost always a solution :grin: but figuring out what it is can be quite a journey.

hjh