Should I always use Atomics?

Hey, newbie here. I just learned that doubles and longs can “tear” due to multi-threadding and to solve it you have to use atomics. So should I always just use atomics? For example if I have a frequency variable for my SynthesiserVoice class, should I use"Atomic<double>"? How much performance do you lose from using atomics and when should you not use it?

The Jeff Preshing’s blog is a good introduction for that stuff < https://preshing.com/archives/ >.

From your question I’m not sure if it’s already clear to you, so simply you should not use atomics if your data is not used by multiple threads. As a lot of tasks you perform are just performed by one thread in most cases you shouldn’t make everything atomic just to be sure, using an atomic should be an intentional design choice for only a few values.

As long as you don’t launch additional own threads in your plugin or application – and I think if you did, you would already be at the point where you could answer this question yourself, so lets just put this case aside for now – you can expect a juce audio application or a plugin to have 2 threads.

One thread is the realtime thread that will do all work related to processing audio and midi. This thread will only call the processBlock function and nothing else.
The other thread is the message thread that will do all the rest: Creating and destroying your plugin editor or application GUI, calling all the Component related functions like paint and resize and all callbacks from buttons and sliders that are triggered because of interaction with the GUI.

So if you have values you set from the GUI through sliders and read from within processBlock they will cross the thread-boundary, so here it is a good idea to make them atomic. But if you have values that are updated inside processBlock and will only be read again in the next processBlock call there is no need to make them atomic.

Regarding performance, this is not so easy to answer, there are operations that don’t need any more CPU cycles to be executed on a modern CPU compared to a non-atomic operation, e.g. setting a bool or a 32 Bit value, however operations on other types might lead to a slightly greater overhead. I think You shouldn’t bother about performance but more about stability and dig deeper if you should encounter performance issues. Most likely other thinks will consume a lot more resources.

I hope this cleared things up a bit for you!

2 Likes

Just wanted to point out, I recently experienced different behaviour when I wrapped a bool in atomic. So it makes a difference even for types, that you would never think of how that would not be an atomic operation.

But apart from that, I would second everything you wrote, only use atomics, where you share information amongst threads and always be clear in your design, what is executed by which thread.

I don’t know whether this is the reason for this different behaviour after making a bool atomic, but atomics in c++ have the positive side effects of ensuring sequential consistency, also when viewed from another thread. In other words:

bool number_is_written = false;
int number = 12;
-----------------------------------
Thread 1:
number = 5;
number_is_written = true;
-----------------------------------
Thread 2:
if( number_is_written )
    assert( number==5 );

Here, the assertion can actually fail, according to the c++ standard. However, if you make number_is_written a std::atomic<bool>, then the assertion can not fail. At least according to the following long-winded article:

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2007/n2480.html

1 Like

Thank you @PluginPenguin for the explanation! That clears everything up :slight_smile:

Related side question:

If I have an array that’s values are being used across multiple threads, should I make the array itself atmoic, or make the values inside the array atmoic?

e.g.

std::vector<Atomic<float>>
// or
Atomic<std::vector<float>>

Depends on what you want to secure… the first one doesn’t help you against resize, it could remove an atomic you are reading the very moment.

The second only makes sense, if you exchange the whole vector on a regular basis IMHO

I would do neither but add a critical section on the access methods to the vector, that I know will change the data or size.

(with one exception I have in my own meters code: I use std::vector<std::atomic<float>>, when I know, the size only changes in prepareToPlay and after that the audio thread writes the current RMS for each channel and the meter reads it)

Doesn’t even compile, but even if it did, the compiler would likely just insert full mutex locks around the operations. Which would be worse than just using your own mutex, because the compiler generated mutex would be invisible “magic” outside your own control.

Well I guess that’s the shortest answer then haha!

Also worth mentioning is the existence of std::atomic::is_lock_free(), which can be used to check if your assumptions regarding the atomic behaviour are correct. You probably don’t want to use mutex-based (non-lock-free) atomics across the boundary of a realtime audio thread.

https://en.cppreference.com/w/cpp/atomic/atomic/is_lock_free

1 Like