Best way to handle destruction of object passed by reference to async callback

I was wondering what would would be the best way of passing an object reference (of an object that might be destroyed when the callback happens) to an async callback, and I thought this might be helpful for others.

TL;DR: use juce::WeakReference and JUCE_DECLARE_WEAK_REFERENCEABLE when callback and dtor of the object happen on the same thread, else use std::shared_from_this?

First I found std::enable_shared_from_this.
By inheriting from std::shared_from_this<MyClass> I can always receive the shared pointer to this object by calling shared_from_this() in class methods.

I could then make the ctor of the class private and use a factory method to create a shared ptr to the object to make sure its always created as a shared ptr.

Now the object can internally create weak ptrs to itself safely.

Example:

void foo ()
{
    std::weak_ptr<MyClass> weakSelf = shared_from_this();

    auto asyncCallback = [weakSelf]()
    {
        auto strongSelf = weakSelf.lock();
        if (!strongSelf)
            return;

        strongSelf->bar();
    };


    juce::NativeMessageBox::showAsync (juce::MessageBoxOptions::makeOptionsYesNoCancel (
                                           juce::MessageBoxIconType::WarningIcon,
                                           "Title",
                                           "Message"),
        [asyncCallback] (int result) {
            if (result == 2)
                asyncCallback();

            return;
        });
}

But there is something potentially better (depending on the usecase), juce::WeakReference.
Its a std::weak_ptr on steroids that does not require a shared pointer, it just returns nullptr if the object is destroyed.

By adding the macro JUCE_DECLARE_WEAK_REFERENCEABLE to the class declaration, you can create a weak reference to your object from anywhere.

First I thought Julian just had a bigger brain, but the problem here is that these weak references do not avoid any destruction of the object from any thread.
If the object existed when checking for nullptr in the callback and later gets destroyed while the async callback is still happening, this wont work.
So only use WeakReferences whenever you are 100% sure that the async callback and the destruction of the object happen on the same thread.

But from my current understanding, using the first approach is threadsafe (because reference counting in shared_ptrs is) as long as every thread has its own shared/weak_ptr instances. Of course this does not mean that editing the state of the actual object in it is more or less threadsafe in any way.
Im uncertain and still learning about the last paragraph so please correct me!

1 Like

juce::WeakReferences does not work well in multi-threading scenarios as far as I know. It’s often used on the UI/main thread. Raw async calls are dangerous too because of the deconstruction problem.

The only safe way I found, is to wait in the deconstructor until all async tasks are finished. Because of this, I’m using the juce::ThreadPool for all multi-threaded asynchronous operations. It does this out of the box if you create it as an instance variable of an object that has the lifetime of the plugin.

You can pass a function to it. It also works great as a working queue when you set the number of threads to one.

For longer tasks, there are also ways to signal that it should end as fast as possible, for example, when the user or the daw wants to remove the plugin.

 threadPool->addJob([this, file] {  loadSample(file); });