Juce::Value and thread-safety


#1

Hi,

I've been struggling with a crash in an audio plug-in when running in Ableton Live! It occured when modifying a parameter by moving Ableton-provided slider. It turned out that Ableton calls parameter change method on a thread other than main one. As my plug-in relies heavily on juce::Value, any parameter change results in juce::Value::setValue call. This queues a value change message for future asynchronous delivery, which is performed by the main app thread. This is where the problem occurs.

It seems that SharedValueSourceUpdater::sourcesNeedingAnUpdate and SharedValueSourceUpdater::sourcesBeingIterated need to be protected against concurrent access to avoid the crash in my case. By using scoped locks in SharedValueSourceUpdater's update, valueDeleted and handleAsyncUpdate (the part where sources are swapped) methods I could fix it, but this requires the change in Juce code. Do you think there's a workaround? I definitely want to use juce::Value - I like it's listener notification mechanism which makes my (already complex) code a lot less complicated. There doesn't seem to be a way of making juce::Value thread-safe (no macro or template argument I could use to provide a critical section or whatever).

Thanks

Hubert Pietrzykowski


#2

That's right - Values and ValueTrees are definitely not thread-safe!

But I've got a work-in-progress class which is a an AudioProcessorParameter that is backed by a ValueTree, but which is also lock-free and thread-safe. I'll publish that hopefully very soon when I'm happy that it's got the right structure and hopefully it should do what you need. The principle is very simple though - each parameter has a raw copy of its value which is used by real-time code, and when a non-GUI thread changes it, it sets a flag that is picked up by a Timer to copy the new value back to the ValueTree. And when the ValueTree changes, the raw value gets updated. It's impossible to use locks to allow non-GUI threads use the ValueTree - that would cause all kinds of glitches - you just have to make sure that only the GUI thread ever has access to it.


#3

Actually, I just realized that SortedSet can be made thread-safe by passing a CriticalSection as template argument. So, the issue can be fixed by replacing the SourceSet typedef (in SharedValueSourceUpdater, juce::Value.cpp) with

typedef SortedSet<Value::ValueSource*, CriticalSection> SourceSet;

This, while simple, still requires changing the juce::Value.cpp file, though - there's no way to declare juce::Value as thread-safe.

 

Hubert Pietrzykowski

 


#4

Honestly - don't try to fix this with locks, you can't lock things on an audio thread or you'll cause all kinds of nastiness.


#5

Jules,

Thanks for prompt reply. I'm aware of all the bad things that can happen when using locking on an audio thread - beginning with thread sync tools overhead to priority inversion. You must have misunderstood me, though. The call I'm receiving from the host is a parameter change. Host should never call parameter change on an audio thread. Ableton doesn't. It apparently just uses some helper worker thread to dispatch the messages initiated by Ableton-provided slider movements, for whatever reason (I'm talking about those generic sliders you can access by clicking little arrow next on the plug-ins top bar and then using Ableton's 'configure' feature.) So this is not an audio thread, but it's not main app thread either. I should be allowed to use thread synchronization tools on a thread which calls plug-ins control code (parameter change). Unfortunately, the fact that Juce asynchronous message dispatch runs on the main thread, while the messages are pushed to the queue by another thread results in problems (crash). So I need to synchronize them. The question is how, without the modification I mentioned? Am I overlooking something?!

Thanks

Hubert Pietrzykowski 


#6

No, I didn't misunderstand. Many hosts, including Tracktion, will send parameter changes on the audio thread, and you have to deal with it. And even that wasn't the case, these calls are obviously time-critical, so locking would still be a bad thing.

Lock-free is the only way to do it. I'll try to get my utility classes out there asap, which should help.


#7

Really? Sending parameter change on an audio thread sounds really scary to me (didn't know there are hosts that do so).

Anyway, if this is the case I could probably replace the critical section with a spin-lock to achieve the same goal without locking.

Thanks!

 


#8

Sending parameter changes immediately before the audio process call is the only way the host can guarantee that a plugin will use those parameter values for that block of audio.

I could probably replace the critical section with a spin-lock to achieve the same goal without locking.

Ahem.. It's called a spin lock because it's a lock.

I'm not sure why don't seem to believe me when I say that this stuff has to be lock-free. If you think a spin-lock is lock-free, then you really need to learn more about how this stuff works before attempting any time-critical code!


#9

Sending parameter changes immediately before the audio process call is the only way the host can guarantee that a plugin will use those parameter values for that block of audio.

This is true, but not universally true. Parameter change often results in computations that need to be atomic or transactional or whatever you call them - I mean they need to be performed and finished between subsequent render calls, not while render is ongoing. F.ex. recomputing long FIR kernel for linear-phase eq. Doing this on an audio thread just doesn't make sense, it would result in horrible performance peaks. To avoid it, the plug-in would need to queue those parameter changes and use a worker thread to perform computations, probably swapping the filter kernel pointer being used by the algorithm when done. This makes the original reason to call parameter on an audio thread (to make sure the new parameter values become effective on the upcoming block of audio) invalid.

Ahem.. It's called a spin lock because it's a lock.

I'm not sure why don't seem to believe me when I say that this stuff has to be lock-free. If you think a spin-lock is lock-free, then you really need to learn more about how this stuff works before attempting any time-critical code!

Right, right. Agree. Spin-lock is a lock, definitely. What I wrote was clearly wrong here. Sorry. What I really meant was that using the spin-lock instead of critical section will avoid the overhead coming from thread suspending/waking.

Now, please think about it in the context of this particular case: SharedValueSourceUpdater. The audio thread, which is only setting a juce::Value will hopefully only spend very short amount of time in a spin-lock, if any - it's the time needed by a dispatch (main) thread to perform a swap (as part of handleAsyncUpdate):

sources.swapWith (sourcesNeedingAnUpdate);

Well, the main thread could potentially get stuck in this line because some other higher-priority thread may be given CPU time by scheduler, this is true. I just can't think of any other way of doing this other than using truly lock-free techniques, such as lock-free FIFO, but this would probably require lock-free multiple-producer/single-consumer FIFO and I don't even know such thing exists.

The question is: what to do? I realize I need to re-think my design. What are the alternatives then? Can I post an action message on and audio thread (as part of parameter change)? Is it thread-safe and will finish in predictable time (lock-free)?

Thanks


#10

The question is: what to do? I realize I need to re-think my design. What are the alternatives then? Can I post an action message on and audio thread (as part of parameter change)? Is it thread-safe and will finish in predictable time (lock-free)?

No, you can't post messages. I briefly explained in my first post what I was proposing - and like I said, I'll release some code asap.


#11

Geez, I missed your first post totally - it's only now that I read it. It's probably because you answered so quickly! Alright, perfect. So it's basically the GUI thread polling for updated values. Great. I wouldn't have bothered you if I'd read it in the first place. Thanks

 


#12

Did these parameter utility classes make it into the library yet? What would be the suggested way of updating a Value-based plugin parameter today? Still DIY-lock free mechanism, or JUCE has a good solution  out of the box?


#13

Sorry, still fine-tuning them - will release them soon!