I use a lot of
ListenerLists, with many classes defining their own customized listener classes. These listeners tend to act across threads, so I lock the
ListenerLists by defining a
template <typename T> using LockedListenerList = juce::ListenerList<T, juce::Array<T*, juce::CriticalSection>>;
Recently though, I’ve been running into problems with deadlocks. It’s difficult to track down, but I think what is happening is that two threads are calling different listener callback simultaneously, and one of them triggers a new listener to be added to the other. Since both
CriticalSections defined, both threads open two locks, but they call the locks in different orders, hence the deadlock.
Doing some research, I have come across the advice again and again, don’t call virtual functions while locking. Now I can see why!
This isn’t the kind of post where sharing code will be all that helpful (though I can share if you like). Instead, I’m looking for general strategies that I can use to solve the problem. Here are some ideas I’ve come up with:
The normal advice is to use
std::scoped_lockto synchronize all of the mutexes (and to ditch the
CriticalSection). But this doesn’t seem so easy in my case, as the locks are being declared in completely different areas of the code. Right now I don’t know how to synchronize them, although perhaps it is possible.
As someone here is bound to say, I could rewrite my code so that Listeners aren’t being called from multiple threads at all. Perhaps this is good advice, but it’s easier said than done. I’m quite heavily invested in Listeners at this stage, and I started using them specifically to simplify my code. Divesting from them would mean a major code refactor, and I’d really rather avoid that at this stage.
I could write my own
ListenerListclass so that the lock isn’t in scope while the callback is being called. I think this would be possible, although it would probably create other difficulties with object lifetime.
I could erase all of my individual listener classes and instead have one central listener which serves all classes. This would function a bit like a message-board for the whole system, and would ensure that all callbacks are synchronized. This would seem to make things a lot simpler, although it also violates a few design principles (i.e. the code would be less decoupled).
Can anyone share any advice? I’m not attached to any of these solutions yet, and would love to hear what other people have come up with.