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

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