Page 2 of 3

Re: Shared variables and volatile

Posted: Sat Aug 05, 2023 7:14 pm
by ColinP
Note this is different to a conventional event dispatcher. A conventional event dispatcher runs in its own thread, has a thread-safe queue as input and methodically works through the events one by one.

I initially thought this was how Notify() worked but as far I can tell it is nothing more than the switch statement you can see in the VMD code editor.

Re: Shared variables and volatile

Posted: Sat Aug 05, 2023 11:41 pm
by utdgrant
There must be at least one event dispatcher 'behind the scenes' in VM, though, controlling the scheduling of ProcessSample() calls.

Unless I'm very much mistaken, all modules and cables must be triggered by some central process to execute ProcessSample() only once for each 'tick' of the 48kHz sample frequency. I've expanded on why I think this to be the case in another thread. Now whether that is one naive supervisor loop of whether it involves more intelligent use of threads and cores, I can't say. However, all the cable processing and all the module processing MUST take place in alternate atomic 'chunks' for each sample period.

This atomic processing of cables and modules appears to be a synchronous 'polled' process which can be 'hard interrupted' by asynchronous Notify() events. I'm not 100% sure if this ProcessSample() 'dispatcher' can in turn interrupt Notify() events, or whether it's just a one-way pre-emptive scheme. I seem to recall that someone (almost certainly Colin :) ) had amassed evidence which confirmed that it was indeed two-way.

Re: Shared variables and volatile

Posted: Sun Aug 06, 2023 8:23 am
by utdgrant
ColinP wrote: Wed Aug 02, 2023 9:50 pm This is because doubles are stored in IEEE 754 format - so 1 bit for sign, 11 bits for the exponent and 52 bits for the significand. This means that almost all of the "important" information is in just one of the 32 bit chunks. So if we are unlucky enough for a thread switch to happen halfway, so that reading and writing operations interleave, the error is very unlikely to be catastrophic. Also the Java spec only says that double assignment isn't guaranteed to be atomic, so in some situations it may be.
It's pretty much guaranteed that doubles (and longs) will be atomic if you're using a JVM targetted to a 64-bit processor family and a 64-bit operating system. It would make no sense for the compiler / libraries to split a 64-bit primitive into two 32-bit slices if it can be treated as a single, atomic entity in hardware.

Caveat: You'll spend a lot of time trying to nail down an explicit statement regarding how a specific Java implementation treats doubles and longs. As you say, the Java Spec itself only states that you can't depend on it being atomic, but I think it can be safely assumed with the three main incarnations of VM (Windows / x86-64, Mac / x86-64, Mac / Apple Silicon). Similarly, the Java Spec does not mandate splitting a 64-bit primitive into two 32-bit halves.

Re: Shared variables and volatile

Posted: Sun Aug 06, 2023 8:41 am
by ColinP
utdgrant wrote: Sun Aug 06, 2023 8:23 am
It's pretty much guaranteed that doubles (and longs) will be atomic if you're using a JVM targetted to a 64-bit processor family and a 64-bit operating system. It would make no sense for the compiler / libraries to split a 64-bit primitive into two 32-bit slices if it can be treated as a single, atomic entity in hardware.
I'll answer your most recent post first...

I think you are correct with regards to compiled code. But HotSpot (the JVM used in Voltage Modular) is more complicated in that it begins by executing the Java bytecode via interpretation. Then in parallel it profiles the interpreted execution and by such analysis determines which parts of the bytecode to perform JIT compilation on - the so called "hot spots". It then bit by bit replaces sections of the bytecode with calls to native code. It's extremely clever technology.

This is why elsewhere I said that double and long assignment may start off as non-atomic and become atomic after Voltage Modular has been running for a while.

Re: Shared variables and volatile

Posted: Sun Aug 06, 2023 9:59 am
by ColinP
utdgrant wrote: Sat Aug 05, 2023 11:41 pm There must be at least one event dispatcher 'behind the scenes' in VM, though, controlling the scheduling of ProcessSample() calls.
I suppose you could look at it that way but to me it would be more accurate to describe the system as a producer/consumer pairing implemented by callback.

The way I think of an event dispatcher is that information is pushed into it, while with the ProcessSample() thread information is being sucked out of it.

Let's look at it from a system POV. Let's say we have an external USB audio interface connected to the computer to make it more obvious what's going on. Now what ultimately drives the whole setup is the clock inside the audio interface. The computer doesn't push information to the interface, the interface sucks the information it requires out of the computer.

In earlier threads we've discussed the ProcessSample() thread in some detail (your insight having started this) and ignoring polyphonic cables for the sake of simplicity I presented the following pseudocode...

Code: Select all

while audio buffer is not full
    for each module in patch
        for each input in module
            inputValue = 0
            for each cable connected to input
               inputValue += value from cable (the value being the output of the previous sample calculation)
         run ProcessSample() in module
    store values of any cables attached to the audio output sockets in the audio buffer
sleep until audio driver empties the audio buffer by making a copy of it
The shorthand where I say the thread sleeps is technically accurate but how this sleeping is implemented in practice is that the inner part of this pseudocode is in fact a callback routine that is called from VM, JUCE and/or DAW code but ultimately from the audio driver. In the case of an external USB audio interface the audio driver is split into one part running on the host computer and another part running on the interface's microcontroller.

So conceptually there are two buffers and two threads. One thread (driven via a NMI from a top quality clock in the interface) is pumping data from the live buffer to the DACs and another thread is sucking data out of ProcessSample() via the USB connection and a proxy thread, DAW, JUCE, VM into the backing buffer at a rate that's just fast enough so that when the live buffer is exhausted a buffer swap can take place.
This atomic processing of cables and modules appears to be a synchronous 'polled' process which can be 'hard interrupted' by asynchronous Notify() events. I'm not 100% sure if this ProcessSample() 'dispatcher' can in turn interrupt Notify() events, or whether it's just a one-way pre-emptive scheme. I seem to recall that someone (almost certainly Colin :) ) had amassed evidence which confirmed that it was indeed two-way.
Yes it can, if ProcessSample() calls SetValue() on a VoltageComponent object, but modules that use what I call a linear flow model don't do this.

CA eventually added a SetValueNoNotification() method to the API that gives us an option to avoid the notification but then; as the regular model is for information to be transferred from the user interface (and the persistence mechanism that automatically preserves the state of controls across sessions) to ProcessSample() via Notify() handling events like Knob_Changed and ButtonChanged; one has to set up an independent mechanism to replace this unless ProcessSample() determines the entire state by polling VoltageComponent objects.

Sorry about that last sentence being so long. :)

I should add that there is a difference between ProcessSample() invoking Notify() and ProcessSample() interrupting a thread that is invoking Notify(). Even in a linear flow model the ProcessSample() thread will routinely interrupt any thread including threads that are currently invoking Notify(). But only in a non-linear model does ProcessSample() invoke Notify().

And regardless of multiple simultaneous invocations of Notify() alway remember that any thread can interrupt any other thread at any time which is why borkman and I have been tearing our hair out and stressing the need to think about thread-safety.

Re: Shared variables and volatile

Posted: Sun Aug 06, 2023 2:10 pm
by AllanH
In my experience, any variable that is accessed from two threads needs to be declared volatile even if it's a primitive data type. As I have the main audio thread and a GUI thread, anything I set/get on the UI thread I mark as volatile.

I have tried removing the volatile to see if it made any difference, and I have to say that I've never experience problems in Voltage Modular. So maybe missing a sample here-and-there is not audible.

However, previously I've worked on larger real-time threaded systems where it absolutely was necessary. At the time I was hunting down a particular threading bug, I found the the JavaVM essentially was caching the (non-volatile) variable as it seemed to conclude that it could not have been changed since being read last. Since then, I simply always mark those variables as volatile.

Re: Shared variables and volatile

Posted: Sun Aug 06, 2023 3:16 pm
by utdgrant
ColinP wrote: Sun Aug 06, 2023 9:59 am Sorry about that last sentence being so long. :)
A wizard makes a sentence precisely as long as he means to. :D

Re: Shared variables and volatile

Posted: Sun Aug 06, 2023 9:02 pm
by poetix
My basic attitude is that doubles passed between threads in VM are mostly like network packets representing individual bullets in an online multiplayer FPS: it doesn’t really matter if occasionally one gets dropped or arrives out of order.
Besides the concern about transporter malfunctions causing the top half of one double to be fused with the bottom half of another, the main point of using volatile is to make sure that temporal sequences of reads and writes aren’t jumbled by caching: if thread B reads after thread A has written, thread B should not receive a stale value. It chiefly matters if thread B might end up sending a calculation based on the stale value back to thread A. Otherwise, if we know that thread B will *eventually* see the updated value, we may not care about strict liveness - volatile means, effectively, “actually, strict liveness matters in this situation”, and its appearance in code should be taken as a signal to pay attention to potential temporal ordering issues. Can anyone think of a scenario in VM module code where it might really matter?

Re: Shared variables and volatile

Posted: Mon Aug 07, 2023 10:44 am
by ColinP
poetix wrote: Sun Aug 06, 2023 9:02 pm My basic attitude is that doubles passed between threads in VM are mostly like network packets representing individual bullets in an online multiplayer FPS: it doesn’t really matter if occasionally one gets dropped or arrives out of order.
It would matter if on my machine that bullet killed you but on your machine the bullet didn't exist so you are still alive.

I actually had to deal with this exact problem when coding a multiplayer game ages ago.

Also Adroit N-Step uses fault-tolerant transmission mechanisms to make operations more reliable when there are a large number of N-Step Aux modules communicating down a long chain.

But I think there is broad agreement that most problems with primitive shared-data are not serious because they either are self-healing, cause just a bit of noise or cause problems that are so rare as to present microscopic levels of risk.

On the other hand a poorly designed bit of code might be expecting an input trigger to be exactly 5 volts or to arrive exactly in conjuction with another logic signal in order to trigger the next section of a song but that trigger arrives as 5.000000000000002334 V or one sample too late instead and your set is ruined.

This discussion is surely about encouraging best practice and being alert to risks that might not be obvious.

Re: Shared variables and volatile

Posted: Mon Aug 07, 2023 11:44 am
by Steve W
With Java and VM is there a difference between audio bits and bytes being randomly lost and midi bits and bytes being randomly lost?