While studying the AudioProcessorValueTreeStateTutorial I came upon the use of atomics for parameter values, like:
std::atomic<float>* gainParameter = nullptr;
So I’ve read about threading, concurrency and atomic and am beginning to understand the concept. I would just like to confirm my understanding is on the right track.
The pointers to these parameters are retrieved in the processor class using getRawParameterValue:
gainParameter = parameters.getRawParameterValue ("gain");
Later on the actual value is used in the processor block, like:
auto currentGain = *gainParameter * phase;
Is the reason using a reference to atomic instead of retrieving the value as a regular local float to make it possible for any interactive change to the parameter being reflected in realtime in the processor block? So that it’s always up to date?
Are pointers to atomic siginficantly less effective than regular float? Meaning, should I only use them when really needed.
Your code is unfortunately not readable, because the blocks between angle brackets are removed by the forum. Please wrap your code in three back ticks on a single line, then the
<float> will reappear.
I fixed it. Strange there’s no button for insert code in the textbox.
This indents the selected block by 4 spaces, which does the same as the three backticks.
I prefer the backticks though, it is more portable to other markups.
The getRawParameterFloat() is a lookup. By having the pointer to the internal atomic float, you avoid this lookup on each processBlock call.
It is an atomic, because it can be changed from outside.
Usually it is done before the processBlock, but some hosts have a dedicated thread which can change the value DURING processBlock. To make this situation safe, it is wrapped into atomic. The atomic wrapper guarantees, that the float is always fully written and not in the process of being written, when it could become garbage (UB).
I was under the impression that floats and anything smaller (bit size) were atomic by nature. I thought I saw that on a JUCE conference video a few years ago. Not so?
Primitive types will still get flagged by Thread Sanitizer. it’s best to just make them
Atomic<primitive type> and be 100% certain its thread-safe.
That is technically correct for most current architectures. But adding the std::atomic around it makes it safe on ALL architectures (and is no overhead, if the type was already atomic on that platform).
Additionally it acts as hint to the optimiser, that this value can be changed from outside the thread, so it must not be replaced by a constant (which the Thread Sanitiser would flag if you didn’t mark it atomic, like matkatmusic pointed out).
It’s not technically true that there’s no overhead in all situations. Depending on the compiler you might get fences or additional instructions generated.
Only when using
std:: memory_order_relaxed are you likely to get exactly the same performance (identical instructions).
However, unless you really know what you’re doing I would advise against using non-sequentially-consistent memory ordering.
The main thing using atomic’s guarantee is that your instructions won’t be optimized in subtle, breaking ways and that load/stores to that value won’t be re-ordered in relation to surrounding instructions.
In short, if you know a variable will be accessed by multiple threads (where at least one of them is a write) use std::atomic. Me and Fabian dig in to this in some detail at our ADC talk: Fabian Renn-Giles & Dave Rowland - Real-time 101 - part I: Investigating the real-time problem space
Great answers, I will watch that video.
For the moment I use the plugin tutorial as starting point. I believe that example consists of two threads, one for processor and one for GUI so that means I should keep atomic in the processor block calculations?
Which argument should be used instead of std::memory_order_relaxed for keeping it efficient and sequentially-consistent?
Just don’t provide one and it will fall back to the default of