I think there is a potential race condition in AudioProcessor::createEditorIfNeeded if called from several threads : multiple editors could be created.
The fix would be to take the lock earlier in the method.
Also, it can be a dedicated CriticalSection imho (or does it absolutely need to be the callback CriticalSection?), so in the fix I proposed hereunder I add a CriticalSection dedicated to the protection of the access to activeEditor member. This will also prevent priority inversion problems (where creating the editor could block audio processing)
Have you actually seen this happen in practice? AFAIK none of the plug-in format wrappers call createEditorIfNeeded() concurrently and will only call it on the message thread.
The issue with your proposed changes is that there is now a more serious priority inversion which can occur if, for example, getActiveEditor() is called in processBlock as at the moment this is guaranteed to be fast and lock-free but with your changes this may block whilst the editor is being created.
No, but better safe than sorry later if things changeā¦
Also, before my changes, createEditorIfNeeded took the callback lock, which can result in a priority inversion and audio dropout
The issue with your proposed changes is that there is now a more serious priority inversion which can occur if, for example, getActiveEditor() is called in processBlock
No, because AFAIK, getActiveEditor doesnāt lock :
Which is the problem. All the effort guarding the activeEditor was in vain, if it is returned here without a lock.
I was wondering if wrapping the pointer into std::atomic would help, but it also just shifts the problem somewhere else. If construction and deletion happen on arbitrary threads, you are in hells kitchen already IMHO.
ā¦which is never safe to do, since the activeEditor can be deleted on the message thread. There is no safe way to call into the editor from processBlock, the locking doesnāt change that.
In the current version, just before actually deleting the editor, editorBeingDeleted is called, and it takes the callback lock, so it is actually safe to use getActiveEditor in processBlock (because the caller of processBlock also owns the callback lock). The downside is that it can create a priority inversion.
In my version, since I was thinking no-one would actually call getActiveEditor in the audio processing thread (it sounds like wanting to do UI stuff in the realtime audio thread, which is not ideal), it becomes unsafe to do that. But the upside is that there is no more priority inversion when the editor is created or deleted (because instead of taking the callback lock, editorBeingDeleted and createEditorIfNeeed take a dedicated lock).
I guess one way to have more safety around this would be to wrap the pointer in a std::shared_ptr (and use the lock to update / copy it, until we have std::atomic_shared_ptr in c++20 ) and getActiveEditor would then return a (copy of the) shared_ptr
I donāt think that is a good idea. shared_ptr moves the destruction of the object to an almost uncontrollable place, so it could happen on the audio thread or any other thread (and the destruction of components is only safe on the message thread).
Another solution, to allow āsafeā usage of the activeEditor on another (UI) thread than the message thread would be to make the lock recursive and provide a method in the api so that the user can take the lock as long as it will use the pointer returned by getActiveEditorā¦ but thatās a bit convolutedā¦
Iām still trying to find a way to reproduce this / regress this in a simple example but in the meantime I must say that it would be also wise to add this as breaking change.
A rationale of knowing (even if itās not thread safe) if thereās a editor is for some calculations youād like to do only when thereās UI.
In our products thereās a code that do things like spectrum analyzers / meters / etc / notify on preset changes / etc only when thereās active editor.
The issue with your proposed changes is that there is now a more serious priority inversion which can occur if, for example, getActiveEditor() is called in processBlock as at the moment this is guaranteed to be fast and lock-free but with your changes this may block whilst the editor is being created.
ā¦which is never safe to do, since the activeEditor can be deleted on the message thread. There is no safe way to call into the editor from processBlock, the locking doesnāt change that.
Indeed, but the lock now makes it impossible for the processor to even ājust knowā if thereās activeEditor without risk of locking on audio thread.
I still donāt see the advantage of the lock on getActiveEditor().
this is inside the AUv3 wrapper.
so if itās assumed to be called only by the message thread than the entire call already is on the message thread.
and both getActiveEditor and editorBeingDelete (and createActiveEditorā¦) has locks.
But even if we had other threads,
getActiveEditor() could return something (with lock) that will be invalid the second after.
I think itās about createEditorIfNeeded, which may be called from multiple threads -thatās the point of the fix. Anyway I donāt see the advantage either -in the worst case getActiveEditor would return nullptr while the editor is being created, which doesnāt seem that terrible.
If this case wouldnāt it make more sense to just lock for createEditorIfNeeded, thus not in getActiveEditor so not breaking the ability to check if thereās a GUI to make calculations for?
Surely you shouldnāt be calling getActiveEditor() from the audio thread?
I know thereās a recently added comment to clarify this:
Note that you should only call this method from the message thread as the active
editor may be deleted by the message thread, causing a dangling pointer.
But even before this was added, calling a method that returns a pointer to an object which we know is managed by a different thread feels like a recipe for disaster.
Wouldnāt a better approach be to simply have any editors increment an atomic which you can use to know if there is an editor?
An alternative would be to add a method to AudioProcessor like hasActiveEditor() which could attempt a try-lock and if the lock can not be taken or it can and activeEditor is not-null then return true.
This sounds like extra bloat to me though and I know youāre trying to avoid having to change existing code because the old behaviour didnāt block.
It sounds like youāre thinking about it the wrong way round.
Instead of thinking āthe audio thread should check whether thereās an editor, so it can decide whether to do some work or notā try rephrasing the task as āthe editor should tell the audio thread what it needs to doā.
So e.g. if you only want to calculate the magnitude when an editor needs it, have a std::atomic<bool> shouldCalculateMagnitude; and make sure an editor set it to true when itās showing, and false when itās hidden.