I think we’re talking at cross-purposes here.
When I’m talking about getting underruns I’m referring to current code without the priority boosting. Mainly because SetPriority isn’t implemented in juce/std::mutex and we don’t know if PTHREAD_PRIO_INHERIT is used in std::mutex.
The fundamental thing about any kind of mutex is that it can take an indeterminate amount of time which is why it could cause underruns (I’m not saying it will though).
As far as I’m aware, using atomics take a determinate number of instructions so don’t have this
indeterminate nature, even if they take a bit longer to execute.
This priority boosting is news to me (at least in the context of general-purposes OSes) so may well change the existing preconceptions.
To be fair, this PTHREAD_PRIO_INHERIT seems to have been added to Linux more than 10 years ago so my fears of priority inversion could be outdated and unwarranted (on macOS/Linux when using JUCE though).
Judging by posts like this: https://sakhnik.com/2017/07/16/custom-mutex.html it looks like PTHREAD_PRIO_INHERIT isn’t standard.
So, yet another thing we have to be aware of that’s implemented differently on different platforms and would have to be done in a 3rd party framework (like JUCE) or via std::mutex::native_handle() in your own code.
The tl;dr here is that if juce::CriticalSection internally did the SetPriority call on Windows I’d be happy. (And if there were some metrics to prove that sleeping threads woke up in a suitably fast time when contended by a higher priority thread).
Sure, and I’ve only talked about using locks to protect data structures to share information between two threads. In other words, simple push/push_back/pop/pop_back instructions. If only doing one of those four operations while holding the lock, then the mutex will not (and can not) take an indeterminate amount of time as long as you have protected yourself from priority inheritance.
Actually, lockless structures have loops in them. Even simple atomic setters and getters have loops in them. So the number of instructions is not determined beforehand. But in practice, it’s not something to worry about, unless the lockless structure is not implemented properly (which is likely considered how complicated they are). The same thing happens when you obtain a normal lock too (at least if it’s optimized), plus spinlocks, so there will always be a bit of non-determinedness.
Also, regarding PTHREAD_PRIO_INHERIT, I want to stress that PTHREAD_PRIO_PROTECT is most likely a better option, at least for implementing message queues.
I think this is the key take away here. If the operations that are guarded by the mutex are determinate, then the whole guarded call is determinate.
Do you happen to know the cost of locking/unlocking a mutex?
This is the classic table I’ve seen in lots of different talks: https://youtu.be/fHNmRkzxHWs?t=2201
Which states ~25ns.
I have also read that some mutex implementations are hybrid and can use a faster futex implementation without a system call if the time they’re held for is short (or even spin for a short time). I’m not sure of the time differences between these operations or even if they’re relevant.
Perhaps you think of hybrids between mutex and spinlocks? I think I read somewhere that the macos mutex was a hybrid between mutex and spinlock, but I don’t know other implementations. If you don’t do much while holding the lock, it’s usually best to use a spinlock anyway, so I don’t know if it’s very common to provide hybrids, but maybe it is.
I’ve written many MIDI processing plugins, and have not experienced the need to use a secondary thread to process MIDI because the CPU requirements are generally a tiny fraction of the overall CPU load. Perhaps you are over-thinking this.
What profiling did you perform to justify the use of a secondary thread in the first place? Have you tried removing all the complicated locking logic and simply handle the MIDI in a straightforward manner on the realtime thread?
Sounds like a lot of midi messages, even if it’s aftertouch. What kind of messages are these?
And I agree with JeffMcClintock, there should be ample time to do the midi handling in the audio thread, if it’s not something extra ordinary stuff you’re doing…
@JeffMcClintock , @oxxyyd : good point actually; I’ve just been paranoid about limiting the work I do on the audio thread - I’ll give this a shot tonight. From the profiling I did do, I noticed the fact that when the audio thread signals the worker tread to do some work, that worker thread actually finished before the next call to processBlock.
@JeffMcClintock@oxxyyd : I was overthinking this - I just tried doing the work I had farmed off to a worker thread, back onto the audio thread and much to my surprise, it works fine! Seems I have been overly cautious! (and my code not too inefficient ). Thx for challenging me!
@oxxyyd: this concerns aftertouch indeed, as well as pitch bend messages. So yeah there can be quite a few within one processblock.