Page 1 of 5

Threading nightmares

Posted: Sun Jul 23, 2023 10:03 pm
by ColinP
In my adventures trying to push VM to do stuff it doesn't want to do I think I've hit a problem that means I'm going to have to write my own version of VoltageKnob and VoltageSlider.

I'm trying to enable users to interactively adjust a "motorized" knob or slider, in other words to brake or move a moving control. Initial testing of this didn't raise any issues but what I've discovered on doing more heavy duty testing is that there appears to be no way to avoid intermittent problems that I'm almost certain are caused by multi-threading conflicts.

What I need to do is interleave changes to a knob or slider's setting caused by user input, DAW automation or MIDI control with programmatic changes. But the changes made to a knob or slider by VM happen in a thread or threads over which there is no access so there's no way to do concurrent synchronization.

Which leads me to the whole topic of undocumented threading in VM.

Generally an event dispatcher like Notify() operates in a single thread with an event queue as input but this isn't how Notify() works. Instead it appears to be a simple switch that's not got a queue and is re-enterant without any concurrent synchronization. So when you handle an event like Knob_Changed then your code can be interrupted not only by ProcessSample() but also by the GUI_Update_Timer handler. Also your GUI_Update_Timer handler can be interrupted by your Knob_Changed handler and ProcessSample(). And you guessed it, ProcessSample() can also be interrupted by your Knob_Changed and GUI_Update_Timer handlers. There doesn't appear to be any conflict control at all.

On top of this, user interface changes such as mouse operations on a knob can't be intercepted (you just get an after the fact event). This all amounts to a thread-safety nightmare and a ready source for bugs in any modules attempting to do non-trivial stuff.

The upside of coding my own knobs and sliders should be the ablility to open them up to dynamic rather than compile-time skinning and to finally get rid of the extremely annoying jump that happens when you click on a slider. The downsides are difficulties with tooltips, value editing and no apparent way to support DAW automation as the right-click behaviour and tooltips are opaque.

I'd be very interested to hear from other devs as to their experiences in any of these matters.

Re: Threading nightmares

Posted: Sun Jul 23, 2023 11:21 pm
by UrbanCyborg
This is why I keep having problems with the multistate toggles I built on top of VoltageToggle; synchronization issues.

Reid

Re: Threading nightmares

Posted: Mon Jul 24, 2023 1:48 am
by borkman
I assumed that notifications and ProcessSample() couldn't be interleaved with each other or themselves. If that's not the case, the potential for really weird errors seems really high. Much of the Java class library isn't thread safe itself and assumes thread safety will be a conscious choice by the developer. Even primitive variable access isn't necessarily thread safe. I've not seen unexplainable behavior yet myself (all errors have been mine and mine alone so far), but the thought of dealing random threading issues makes me a bit queasy.

Can we get a Cherry Audio dev to chime in here?

Re: Threading nightmares

Posted: Mon Jul 24, 2023 8:03 am
by Waverley Instruments
Not to go off on a tangent, but I think a lot of the "fighting Notify()" issues, which I also know all too well, might simply go away if there was an option to make a VoltageComponent non-persistent.

As we know, they currently save and restore state "automagically", whether we like it or not... And a lot of the trouble (for me) occurs with Notify() being fired on state restore.

That's all fine if you have one-to-one relationships between your underlying model and UI, but, as I'm sure you're all aware, it gets messy if that's not the case. And you end up "fighting Notify()"...

Interestingly enough, a similar (to my mind, at least) development environment, KONTAKT / KSP has a make_persistent() function, so it's entirely up to you whether a UI control does the automatic state save / restore.

I for one, think that might be a nice enhancement for VMD and would have certainly made my life a little easier for some projects.

-Rob

Re: Threading nightmares

Posted: Mon Jul 24, 2023 9:37 am
by ColinP
Agreed Rob, the persistence thing is a pain but (I think/hope) I solved that with a properlyRestored flag that brackets any event handlers that need auto-persistence disabling. I then set properlyRestored to false on Preset_Loading_Start and true on Preset_Loading_Finish.

The multi-threading thing is more problematic. I think CA (Dan G) did a great job with the basic design but it's all predicated on a linear flow model where user input modifies UI elements and ProcessSample() reads the values and computes an output.

I'm guessing Reid that you only see problems once in a blue moon?

Mostly the issues only occur quite rarely as it's statistically unlikely for instance that a timer event will interrupt a knob change handler. I only know this happens because I wrote diagnostics specifically to catch it and spent several minutes twiddling a knob until the zones happened to overlap.

So long as there's only one thread changing shared state then things work well enough and even when doing more complicated stuff the problems are intermittent. I also try to write fault-tolerant code so do lots of boundary checks and clamping even when it's very unlikely that they will ever be needed. It helps reduce the likelyhood of those very rare but nasty malfunctions that pop up for no apparent reason.

But this particular problem I'm having is I think only solvable by replacing VoltageKnob and VoltageSlider. It's a pain as there aren't any hooks in the API for some things but I'd rather have reliable operation and sacrifice functionality than the other way around.

Re: Threading nightmares

Posted: Mon Jul 24, 2023 11:57 am
by poetix
I would be minded to post every update I wanted reflected back into the GUI into a concurrent queue, and have the GuiUpdateTimer handler drain everything off that queue and adjust the positions of knobs etc accordingly. It might even be worth starting up a single thread of your own to process the updates. That way reentrancy is strictly controlled: if something else decides a new update should occur while I'm processing a previous one, to the back of the queue it goes! You just have to make sure the queue itself doesn't grow unboundedly...

Re: Threading nightmares

Posted: Mon Jul 24, 2023 1:05 pm
by ColinP
Cheers poetix but I can't see how that would work, although I may well just misunderstand your idea.

The problem is that the thread that handles user interaction with components like VoltageKnob is beyond access - it does its own thing and only then calls Notify() with a Knob_Changed parameter. So there's no mechanism available to intercept its behaviour in order to add synchronization.

And as that thread can interrupt the timer and ProcessSample threads and the timer thread can interrupt the user interface and ProcessSample threads and the ProcessSample thread can interrupt the user interface and timer threads I can't see any way to avoid conflict. Calls to SetValueNoNotification() appear to work almost all of the time but when the re-entrant wind is blowing the wrong way things go wrong. It's a subtle problem that I've only just discovered while doing final checks prior to going into beta with Adroit Custom.

The only solution I can see is to handle everything myself with the inputs being just mouse and paint events.

Re: Threading nightmares

Posted: Mon Jul 24, 2023 1:47 pm
by poetix
It may be possible to reduce the tangle a bit. As I understand it, there are times when you want to set the value of a knob from within some piece of logic running in ProcessSample, and the problem is that you end up in a race with changes to that value in the UI which you only get notified about - potentially in the middle of still doing things in ProcessSample - after they've happened.

Suppose we never directly call SetValue on the knob from inside ProcessSample, but only record a suggestion for a new value somewhere where it can be read later. Now if later on in the same invocation of ProcessSample we want to access the new value we may have a problem, but the answer there may be to decouple our model from the UI - set our internal record of the value immediately, and refer to that, but defer pushing the update to the visual component until later. Now we also have the option of controlling whether a call to Notify() can set our internal value immediately, or whether it too records a suggestion that we pick up and process when it is safe to do so.

It might then make sense to have a function called only when the GuiUpdateTimer fires whose job is to say "what has the UI told us has happened since the last time we fired? And what changes have there been to our internal model?", and reconciles the internal model with the external display. It may be as simple as keeping the most recent information from both sources: the last we heard from ProcessSample, it was setting the value of Foo to 1.0; the last we heard from the external system, FooKnob had received a new value of 0.9. Provided we can order these two events (something a concurrent queue will do for us very nicely, for example) we can decide whether we need to tell the external system to update its view of things, and to what.

Re: Threading nightmares

Posted: Mon Jul 24, 2023 2:01 pm
by poetix
It does make for a more complicated programming model, but the general rule is to treat all the UI components as a separate source of truth from the internal state of your processing engine: take advice from them about what's going on out there in user-land, and let them know when you want something to happen (like an LED coming on, or a knob turning to reflect a new value), but where there's the possibility of conflict do both through an intermediary layer with controlled scheduling rather than by making direct calls across contexts.

Ironically, this moves VM towards an old-fashioned split between control-rate and audio-rate processing, as you may find you only need to attend to the demilitarised zone between the UI and the internal model once every 50ms or so - which is why the GuiUpdateThread is a good candidate for doing that work.

Re: Threading nightmares

Posted: Mon Jul 24, 2023 4:31 pm
by Waverley Instruments
poetix wrote: Mon Jul 24, 2023 2:01 pmthe GuiUpdateThread is a good candidate for doing that work.
Exactly what I do FWIW, sometimes setting flags if need be, that the state of the underlying mode has changed, so the UI should update at the next 50ms update.

I also never do any VoltageComponent SetValue() or even GetValue() calls in ProcessSample(). Better safe that sorry!

-Rob