Anyone who has done a bit of low level audio programming has been cautioned about locking the audio thread. Here's a nice article on the subject.
But it's very unclear to me how to actually go about making a multithreaded audio processing application in c++ while strictly following this rule and ensuring thread safety. Assume that you're building something simple like a visualizer. You need to hand off audio data to a UI thread to process it and display it on a regular interval.
My first attempt at this would be to ping pong between two buffers. buffer*_write_state is a boolean type assumed to be atomic and lock free. buffer* is some kind of buffer with no expectation of being thread safe on its own and with some means of handing the case where one thread gets called at an insufficient rate (I don't mean to get into the complications of that here). For a generic looking boolean type, the implementation looks like this:
// Write thread.
if (buffer1_write_state) {
buffer1.write(data);
if (buffer2_write_state) {
buffer1_write_state = false;
}
} else {
buffer2.write(data);
if (buffer1_write_state) {
buffer2_write_state = false;
}
}
// Read thread.
if (buffer1_write_state) {
data = buffer2.read();
buffer2.clear();
buffer2_write_state = true;
} else if (buffer2_write_state) {
data = buffer1.read();
buffer1.clear();
buffer1_write_state = true;
}
I've implemented this using std::atomic_flag as my boolean type and as far as I can tell with my thread sanitizer, it is thread safe. std::atomic_flag is guaranteed to be lock free by the standard. The point that confuses me is that to even do this, I need std::atomic_flag's test() function which doesn't exist prior to c++20. The available, mutating test_and_set() and clear() functions don't do the job. Well known alternative std::atomic is not guaranteed to be lock-free by the standard. I've heard that it most cases it isn't.
I've read a few threads that caution people against rolling their own attempts at a lock-free structure, and I'm happy to abide by that tip, but how do experts even build these things if the basic tools aren't guaranteed to be lock-free?
I've heard that it most cases it isn't.
You heard wrong.
std::atomic<bool>
is lock_free on all "normal" C++ implementations, e.g. for ARM, x86, PowerPC, etc. Use it if atomic_flag
's restrictive API sucks too much. Or std::atomic<int>
, also pretty universally lock_free on targets that have lock-free anything.
(The only plausible exception would be an 8-bit machine that can't load/store/RMW a pair of bytes.)
Note that if you're targeting ARM, you should enable compiler options to let it know you don't care about ARM CPUs too old to support atomic operations. In that case, the compiler will have to make slow code that uses library function calls in case it runs on ARMv4 or something. See std::atomic<bool> lock-free inconsistency on ARM (raspberry pi 3)