Test for concurrency issues PluginProcessor <-> PluginEditor?


#1

Making a fresh thread for this. I’ve been looking into the complications in sharing data between GUI/message rate threads and the audio DSP rate thread.

Does this look like a reasonable test for tearing/other problems?

// PluginProcessor side
// check the value of param extremely often (>sample rate)
double param {0.0};

void processBlock (AudioSampleBuffer& buffer, MidiBuffer&)
{
    for (int channel = 0; channel < numChannels; ++channel)
        for (int sample = 0; sample < numSamples; ++sample)
            jassert (param == DBL_MAX || param == 0.0);
}

// GUI PluginEditor side (timer running every 1ms)
// Write alternating value to param every 1ms
bool toggle {true};

void timerCallback()
{
    if (toggle)
        processor.param = DBL_MAX;
    else
        processor.param = 0.0;

    toggle = ! toggle;
}

It seems to work on my setup here i.e. it jasserts on average every few seconds. But is this too naive?

Thanks in advance!


#2
  1. It doesn’t really make sense to check the parameter each sample as the wall clock time doesn’t progress linearly as such with respect to the sample timings (which is why people often load the value at the start of the processing value into a local variable)
  2. The parameter needs to be atomic, otherwise your code has undefined behaviour. You can get away with acquire/release ordering. This is very simple using std::atomic<>.
  3. This doesn’t test for tearing, as the x86 arch natively supports atomically reading 8 byte values. This can be checked using std::atomic::is_lock_free() or through a suitable define found here:

http://en.cppreference.com/w/c/atomic/ATOMIC_LOCK_FREE_consts

Lastly, the test will, if you load the value at each check (required if the value is atomic) occasionally fail - but it is well-defined, ie. expected and not UB.


#3

[quote=“Mayae, post:2, topic:20766, full:true”]
It doesn’t really make sense to check the parameter each sample[/quote]
yes indeed it’s an extreme example, non-real world.

[quote]
The parameter needs to be atomic, otherwise your code has undefined behaviour. [/quote]
of course, but I’m trying to force undefined behaviour. Probably my use of the word ‘test’ isn’t the best choice!

[quote]
This doesn’t test for tearing, as the x86 arch natively supports atomically reading 8 byte values.[/quote]
What would test for tearing?
Also why would my jassert’s be tripping?

Interesting thanks! Does this just apply when something is wrapped in std::atomic or to regular types as in the example?


#4

[quote=“saintidle, post:3, topic:20766”]
of course, but I’m trying to force undefined behaviour. Probably my use of the word ‘test’ isn’t the best choice!
[/quote]On desktop machines, you’re gonna have a hard time. CPU designers go to great lengths to make things working, even though they aren’t guaranteed to… The most notable problems with concurrency is the ordering of events, loads and stores, which is where both the CPU and the compiler can be very aggresive and speculative in optimizing. Your code exhibits no ordering situations - that is, you have no conditionals or chains that depends on previous shared variables.

[quote=“saintidle, post:3, topic:20766”]
What would test for tearing?Also why would my jassert’s be tripping?
[/quote]Tearing would only be noticable when loading/storing data types larger than 8 bytes on x86 hardware. Your jasserts are tripping because the compiler may load the param variable again at each check, and the variable may have changed meanwhile.

[quote=“saintidle, post:3, topic:20766”]
Interesting thanks! Does this just apply when something is wrapped in std::atomic or to regular types as in the example?
[/quote]This only applies for types wrapped in std::atomic or exclusively interacted with through the C interface. Technically, the CPU doesn’t care but the compiler needs to know it is an atomic variable to avoid weird optimizations/elisions.

Also note that the optimization level can dramatically and unintuitively change the outcome of ‘tests’ like these, especially if you’re not letting the compiler know you’re dealing with atomics.


#5

Thanks this is great info.

I’ve been directed towards your cpl/ConcurrentServices.h by a friend, a lot to learn from there too.

In terms of the juce::AudioParameterFloat type which does not seem to have anything marked atomic, are there situations where GUI->Processor interaction would have adverse effects?


#6

Not really on desktops, as all x86 CPUs memory models are strongly ordered. For other platforms (like ARM) it is indeed a problem if you have logical dependencies on your parameters.

Now this is only the architecture. The biggest problem is fighting the compiler optimizations, that are utilizing the inherent UB in concurrently accessing these parameters. So yes, it is actually a very real problem. You can see this more technicaly reply here, but it still seems no action have been taken from the JUCE team:


#7

Great thanks! I did search before posting but did not find that thread.