#include <vector>
void violation() [[clang::nonblocking]]{
std::vector<float> v;
v.resize(100);
}
int main() {
violation();
return 0;
}
If you've worked on real-time systems, you know they must produce results within a predictable time. Think of any modern audio system, from your phone to the speakers and mixing desks at a live show. They may record sound and play it back, instantly. The software must process the signal predictably, and finish within a deadline. Poorly designed real-time systems can even cost lives, particularly in cars, defense systems, and aircraft.
In a real-time audio system, audio is usually processed at a fixed number of samples per second, divided into buffers of N samples. The program receives each buffer in an audio callback function, which takes the incoming audio and outputs new or modified samples.
A live sound desk may run at 48 kHz with a buffer size of, for example, 64 samples. That gives the program 1.33 ms to process each buffer. If it takes longer, the audio can't be delivered in time, and you hear a glitch: a click, a dropout or a loud pop. In the worst case, that can damage equipment or hearing.
To make sure code finishes in time, it must be deterministic and predictable. It can't do anything where the time it takes is unknown. That includes any OS call, such as allocating memory, file or network I/O. This includes code that has to wait, such as a mutex lock.
Today I want to look at a tool introduced in Clang 20 (March 2025): RealtimeSanitizer, or RTSan. It's a runtime sanitizer for C and C++ that detects real-time violations. Functions you mark as [[clang::nonblocking]] are treated as real-time, and RTSan reports any call to a non-real-time function from them. To enable it, pass -fsanitize=realtime to the compiler.
Here's an example from the Clang documentation:
#include <vector>
void violation() [[clang::nonblocking]]{
std::vector<float> v;
v.resize(100);
}
int main() {
violation();
return 0;
}
Resizing the vector allocates memory, which is an OS call with unbounded time. Because violation() is marked [[clang::nonblocking]], the sanitizer reports:
Intercepted call to real-time unsafe function `malloc` in real-time context!
Similarly, with a mutex:
#include <mutex>
std::mutex mutex;
void violation() [[clang::nonblocking]]{
mutex.lock();
}
int main() {
violation();
return 0;
}
Not all violations are this obvious. Here's a program from my new eBook 25 C++ Cases That Will Humble You. Can you spot the violation?
#include <algorithm>
#include <cmath>
#include <print>
#include <span>
#include <vector>
void applyNoiseGate (std::span<float> buffer)
[[clang::nonblocking]] {
auto exceedsThreshold = [] (float sample) {
static const float threshold =
std::pow (10.0f, -48.0f / 20.0f); // -48 dB
return std::abs (sample) >= threshold;
};
if (std::ranges::none_of (buffer, exceedsThreshold)) {
std::ranges::fill (buffer, 0.0f);
}
}
int main() {
std::vector quiet { 0.0001f, -0.0002f, 0.0003f };
std::vector loud { 0.1f, -0.2f, 0.3f };
applyNoiseGate (quiet);
applyNoiseGate (loud);
std::println ("{}", quiet);
std::println ("{}", loud);
}
Whether this code violates the real-time rules depends on the standard library it's compiled with. Compile it with -stdlib=libc++ (the default on macOS), as the Compiler Explorer link does, and RTSan reports:
Intercepted call to real-time unsafe function `pthread_mutex_lock`
The problem is the static keyword. Since C++11, the standard requires the initialization of a function-local static to be thread-safe, in case two threads try to initialize it at the same time. To meet that requirement, libc++ uses a mutex. So the first call to applyNoiseGate locks a mutex, which breaks the real-time rules.
Real-time safety is more than avoiding blocking calls. Take a spinlock: it spins the thread until it can take the lock. It doesn't call any OS function, so it looks harmless, but it can still make the thread wait, and the thread can miss its deadline. As far as I know, RTSan can't detect this.
To talk about that difference, the terms used are lock-free vs. wait-free. An algorithm is wait-free if every operation finishes in a bounded number of steps, and lock-free if the system as a whole always makes progress, even when one thread is delayed.
Even if an algorithm is both lock-free and wait-free, it isn't automatically real-time safe. If the math simply takes too long, you still miss the deadline. True real-time safety goes much further than removing OS calls and locks. But it's a great first step, and RTSan makes that step much easier.
No matter how carefully you write your code, real-time violations can still go undetected. Tools like RTSan can help us identify and catch these hidden issues.
Want to learn more about real-time safe C++? I can recommend the conference talks by Fabian Renn-Giles, specifically the talk Real-time Confessions in C++ - Fabian Renn-Giles - ADC23
The noise gate puzzle is case 23 of my new eBook 25 C++ Cases That Will Humble You. Every case is based on a real kind of bug, with an in-depth explanation and a Compiler Explorer link, so you can run it yourself.
Subscribe to the C++ Doctor e-mail list below:
Thank you for subscribing!
If you enjoyed this, you might also like my interview with Niels Lohmann, C++ expert, speaker, and author of C++ Software Design.