You are right: the synchronisation of the parameters between the UI thread and the audio thread is not clean. In fact, according to the C++ standard, it is undefined behaviour to access the same memory address from multiple threads without using
std::atomic (or JUCE’s Atomic) or some kind of locks (which is a bad idea in your audio thread as your audio thread should be lock-free - so use atomics instead).
However, in practice, it currently works to share a float like this in plug-ins. This is because - on all platforms that support desktop DAWs (basically x86) - reading/writing a float (or anything that is 64-bits or less) is a single machine instruction and is atomic , i.e. it will never be in a teared state - an update to a float was either executed successfully or not.
In fact, on these platforms, I’ve found that
std::atomic will generate the exact same load and store machine instructions compared to not using
std::atomic at all. On arm, however, the situation is different: there you are likely to need
std::atomic (or JUCE’s
Atomic class) as floating point access is not guaranteed to be atomic.
I say currently, because future (and in rare cases, even current) compiler optimizations may break your code if you do not use
std::atomic. Consider the following example:
void processBlock (...)
float localCopy = myParam;
for (int i = 0; i < numSamples; ++i)
sample[i] *= localCopy;
In this code, you want to avoid tearing and therefore copy the parameter value to
localCopy before you do any processing. For arguments sake, let’s say your processor depends on
localCopy being constant during the
for loop - otherwise it will crash.
The above code will currently work on x86 - even if you update
myParam from the UI thread. However, technically, the compiler optimizer is allowed to replace any reference to
localCopy with a reference to
myParam . This could be because the compiler optimizer wants to save stack-space or save valuable registers for other operations and therefore tries to optimize away the storage for
localCopy. The result is that
localCopy will not remain constant during the loop if
myParam is updated from the UI thread.
The standard says that the C++ compiler is allowed to do this as changing
myParam outside of
processBlock would be undefined behaviour. Therefore it can assume that myParam will remain constant in
processBlock as the compiler sees that
processBlock itself (or any of the sub-routines it calls) does not change
std::atomic explicitly tells the compiler that
myParam may change from outside of the thread and therefore the above optimization is not allowed.
In essence, if you want to play it safe and be future-proof, use JUCE’s
Atomic class or
std::atomic - currently, however, it is very unlikely to make any difference for plug-ins running on desktop OSes.
 There are some other requirements for it to be atomic like proper padding and alignment. The compiler will do that for you anyway though unless you are using some crazy pragmas
 If an operation is expressed as a single machine instruction, it does not mean that it is atomic. However, load/stores of anything that is a 64-bit word or smaller is atomic on x86 when properly padded and aligned.