-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
use_counting_ptr, a threadsave callback solution #1713
base: main
Are you sure you want to change the base?
Conversation
Could you explain what specific problem motivated you to start working on this? Does it solve a problem that came up in #1675? |
I am experience rar crashes when shutting down Mixxx.I was never able to reproduce it under gdb, so I have never reported a bug. One idea is that this is caused by explicit direct connections across threads. Qt does not prevent to call a callback from an already deleted object, if there is a race between the callback call from one thread and the delete call from an other thread. |
We use these direct connection for the engine thread COs |
I cannot use this currently, because of pending #1700 |
Can we come back to this in a few weeks after the 2.2 feature freeze? |
I like to have this included. |
Why? |
I actually waiting for a review of #1700 more urgently. Our control objects where becoming a bit messy, and I think it is a good idea to clean them up in small steps instead of one giant PR. |
I'm not questioning that we should clean the code, but I don't think < 2 weeks until a feature freeze is a good time to be focusing on it unless it fixes an important bug. |
@rryan Do you want to have a look here? |
@uklotzde do you have interest to wrap your head around this? I would like it to fix the a probably cause of the crash on close fix I experience some times. |
Honestly I still don't get the point by reading the code?? Couldn't this be implemented in a simpler and more comprehensible way? I'm not able to decide if this works correctly in all possible cases. My feeling is that we might try to cover a design flaw by mitigating it with just another half-baked and fairly complex solution. I noticed that the implementation has some technical flaws, e.g. the constructors are inconsistent and behave unexpectedly when invoked with a nullptr. I came to the conclusion that we should use asynchronous signal/slot messaging whenever possible. Why plain callbacks? Trying to work around Qt's foundation and patterns is one of the main problems in our code base. I'm just criticizing myself, because this is what I would've done when rewriting the multi-threaded analysis code now. But I won't rewrite it a second time in C++ ;) The global track cache is a slightly different story. It's also far too complex, but since track objects appear as shared mutable state everywhere we didn't have a choice without rewriting large parts of the application. |
Thank you for looking into it. This pointer was my idea for solving a crash on shut down when this https://github.com/mixxxdj/mixxx/blob/master/src/control/control.h#L164 object is deleted while an other thread is just about to call callback. I would love to use a Qt solution for it, but it has not. Direct connections have no safety net in QT and suffer basically the same issue. My idea was to use this pointer type to workaround this limitation, in an almost look free way. Do you have alternative ideas? Is there a chance to tun this into a simpler and more comprehensible solution? That would be great. |
Would you please include an example were this technique is applied? I need to see it in action. We need this anyway. Adding unused and untested code to the repository is discouraged ;) |
I have started to use the new pointer here: https://github.com/mixxxdj/mixxx/blob/master/src/control/control.h#L164 Many instances can be replaced by ControlProxyLt #1717 |
This PR is marked as stale because it has been open 90 days with no activity. |
this can probably be closed, right? |
Yes, the underlying issue still exists though. |
I don't really understand it though. Maybe you can explain it again? |
When you call a member function form one thread and in the mean time an other thread deleted the object, we experence a crash. This happens if the caller does not hold a strong reference to the object until the call ends. That obvious so far. Normally you would use a combination of a
This pointer here allows to borrow the pointer without that the original owner looses the ownership. It is guranteed that the desructor called in the creator thread is delayed unitl there is no user anymore for instance a callback has returned, than finally the detruction the object and itself can continue. This issue happens during shutown of Mixxx when we hav a dangling pointer in the QT event queue which might happen in |
but why does it matter from which thread the destructor is called? This is only relevant in realtime safe-code where we IMO shouldn't use these patterns anyway. |
Right, thats what |
It is required that the creator thread is suspended, until all object that it has to delete are deleted. I would be happy to discuss with you alternative better to understand ideas if you have interest. Functional requirements are:
|
I'm getting confused now. Is this about the thread in which the event loop is placed is being shut down or about the object being deleted too early? |
Every Qt object lives in a dedicated thread. When we shut down Mixxx, both is disposed. The solution here works by suspending the object destructor, before the thread is disposed as well. |
Right. This essentially an architecturally rooted use-after-free issue. Why does this happen in |
Because we use direct connections into the engine thread. The engine thread has no Qt event loop. Qt has probably not considered direct connections in that case. |
well, direct connections are just plain function calls afai understand. So its an architectural issue if we still have access to these objects after they're dead. Wouldn't QPointer fix this since it nulls out all the references once the QObject goes out of scope? |
The issue with QPointer is that it does guarantees ownership. You can check it for null before calling the callback, but the crash still happens if the object is deleted after the check during the runtime of the callback itself. |
Ah I slowly see how this works. I still don't think its a good design though. Its a bandaid fix on top of the larger architectural issue that is directly calling member functions of objects living in other threads. |
You have always the situation that an object that provides shared memory for concurrent access needs to outlive both threads. The architectural issue here is the Qt object tree where a shared ownership is not possible and the nature of Qts direct connections that do not have a safety net for such situations. Even without Qt, we don't want to pass the ownership of an object to the engine thread, because that it has to dispose it as well and free() is not lock free. Our discussion proves, that the solution implemented here is not self explaining. I still think that the building blocks are valid. So maybe you can recommend a better design? |
Well, on shutdown that would be okay IMO. We don't need the (I assume audio?) engine to remain realtime safe then. But I don't think thats a solution we should go for either. I'm not too experienced with concurrency unfortunately so take this with a grain of salt but IMO the way C++ models this would be essentially akin to "structured concurrency" where a thread is a resource and we use RAII to manage it. That implies that the child thread must outlive its parent thread and the child thread must outlive any objects created within it. The thread can only shut down once all the objects created within it have also been destroyed (and any references to them have been invalidated of course, otherwise it would be a use-after-free). These constraints lend themselves nicely to RAII management which is one of C++'s strengths. int main() {
auto someMainObj = ...;
auto engine = std::jthread([]{
someEngineObject = ...;
// communicating with someMainObj is easy (inside-out)
// the other way is intenionally hard because we need
// to workaround the nice automatic storage duration.
// But since we have now transformed this into a regular
// lifetime problem, our usual lifetime tools would suffice.
// in this case, the solution would be to created a `shared_ptr`
// in this thread and hand out a `weak_ptr` to the parent thread.
});
} |
I do not understand. How does this translate into a solution for Mixxx? The issue we experience here is real and to fix it before doing a major refactoring. Do you habe a plan for this? What is the issue with this solution regarding the picked primitives and naming? |
I don't understand the particular problem in mixxx well enough to offer an alternative right now. I was just posting on how this is generally supposed to be solved. If you can link me to a backtrace of the issue you're describing I may be able to understand it more. The problem I have with Regardless, I wasn't trying to revive this PR, I simply went through our PR backlog by oldest PR and tried to find any that have gotten stale enough to the point where they're worth closing. This seemed like a candidate but apparently isn't. I don't have interested to revive this right now. |
This is a wrapper class to prevent to delete an object that is still in use
in a multithread environment. It allows to use the object from mutible
threads without locking. The final clear is however locking if the object is still
in use.
This can be used to implement callbacks to an object with shorter livetime. It is
a replacement for Qt's direct connections, which are not save across threads
in this case.
Usage:
call set(po) and clear() to store and remove a pinter from the object.
call the callback from a scope like this