添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
相关文章推荐
想出国的羽毛球  ·  日产Formula ...·  1 年前    · 

If I understand juce::WeakReference correctly, its use case is for when the referenced object’s deletion and usages do not happen in concurrent threads.

Is there a recommended solution or standard class for a thread-safe variant of it? I.e where you scope the usages with a lock to make sure that the object isn’t deleted while you use it.

For now I’m using this small utility that I created:

// SafeRef.h
/////////////
#pragma once
// This module provides a weak-reference mechanism,
// which unlike JUCE's WeakRef allows for safe locked access.
#include "JuceHeader.h"
template<typename T>
class SafeRef : public ReferenceCountedObject
public:
    typedef ReferenceCountedObjectPtr<SafeRef> Ptr;
    SafeRef (T* owner = nullptr)
        : owner_ (owner)
    ~SafeRef()
        // If owner_ wasn't reset to nullptr then the user forgot to call reset()
        // in their destructor.
        jassert (owner_ == nullptr);
    void reset (T* owner = nullptr)
        ScopedWriteLock l (lock);
        owner_ = owner;
    // A scoped read-only access to the reference.
    // For additional write access one may use additional ScopedWriteLock on the reference's `lock`.
    class ScopedAccess
    public:
        ScopedAccess (SafeRef::Ptr& ref, bool tryLock = false)
            : ref_ (*ref)
            if (tryLock)
                didLock = ref_.lock.tryEnterRead();
                ref_.lock.enterRead();
                didLock = true;
            owner_ = didLock ? ref_.owner_ : nullptr;
        ~ScopedAccess()
            if (didLock)
                ref_.lock.exitRead();
        T* operator*()
            return owner_;
        T* operator->()
            return owner_;
        operator bool() const
            return owner_ != nullptr;
    private:
        SafeRef& ref_;
        bool didLock;
        T* owner_;
    // The lock is public, so owner can use `enterWrite` etc with it freely.
    ReadWriteLock lock;
private:
    T* owner_;

Cheers, Yair

I don’t think you can ever do this with an intrusive reference counted pointer. In your example, you can still end up in dodgy territory if you enter the sub-class destructor, then take a ScopedAccess. You might technically get away with this but I think it’s UB to use an object that is currently being destructed. (At the very least it’s difficult to reason about).

If you want to use juce::ReferenceCountedObject I think you need to make sure all your objects are deleted on a single thread which everyone agrees on. That way you know if an object is partially deleted.

But to be honest, the safest bet is to use std::shared_ptr and std::weak_ptr. They use atomic ref counts to ensure you can only dereference through a valid std::shared_ptr, even if obtained via a std::weak_ptr.
The only downside to this is that the memory storing the object will only be freed after all the weak_ptrs have been destroyed. This means you may need to poll them occasionally to reset them.

I guess my example class is not documented enough and caused some misunderstanding. Here’s how I use it:

I don’t inherit from SafeRef. A class X which needs weak-reference support contains a SafeRef<X>::Ptr member which it initialises in its constructor and invalidates (with a ref->reset() call) in its constructor (this is similar to having a juce::WeakReference<X>::Master).

Objects referencing the mentioned X also have a SafeRef<X>::Ptr member and use SafeRef<X>::ScopedAccess whenever they want to read from the referenced object.

Yeah, that’s what I thought but you can still end up using the object during destruction?
Is this the kind of thing you had in mind? (It’s quite tricky to come up with a short threading, example…)

struct TestSafeObject
    TestSafeObject() = default;
    ~TestSafeObject()
        // [1]
        safeRef.reset();
    SafeRef<TestSafeObject> safeRef { this };
    using TestScopedAccess = SafeRef<TestSafeObject>::ScopedAccess;
// Elsewhere
std::thread thread;
    TestSafeObject testSafeObject;
    auto thread = std::move (std::thread ([testSafeRef = testSafeObject.safeRef]
                                              if (auto access = TestSafeObject::TestScopedAccess (testSafeRef)) // [3]
                                                  // Do some stuff [4]
} // [2]
thread.join();

In the above, it’s possible that [2] will be reached, enter testSafeObject's destructor but not reset the safeRef, then [3] happens on the background thread giving a valid access object.
Then [4] is being called whilst testSafeObject is sitting in its destructor waiting on the lock.

As I said, this is extremely unlikely to happen and depending on your vtable, compiler, scheduler and which way the wind is blowing may or may not actually cause problems…

However, I think if [4] calls virtual methods of TestSafeObject, you can end up with problems.

(Disclaimer, above code typed directly in and not tested)

It should be SafeRef<TestSafeObject>::Ptr.

This way, after the referenced object is destroyed, the actual SafeRef<TestSafeObject> is still alive and well (but it knows that the referenced object itsn’t available because its owner_ member is nullptr). The SafeRef object will be destroyed when both the reference-able object and all of its referencers are destroyed.

SafeRef<TestSafeObject>::Ptr safeRef { new SafeRef<TestSafeObject> (this) }; using TestScopedAccess = SafeRef<TestSafeObject>::ScopedAccess; // Elsewhere std::thread thread; TestSafeObject testSafeObject; auto thread = std::move (std::thread ([testSafeRef = testSafeObject.safeRef] auto access = TestSafeObject::TestScopedAccess (testSafeRef); if (access) // [3] // Do some stuff [4] access->blahblahblah(); } // [2] thread.join();

I think that how you intend it to be used?

But the problem remains, you can still end up in the situation where TestSafeObject is being destructed but the safeRef member is still valid so grants an owner_ that is in the middle of its destructor.

It’s a small race, and possibly benign (there’s debate over whether any race can be benign though) but the only way to avoid any race is to have external pointer with strong and weak counts. That way, it’s impossible to start destructing the object before the ref count gets to 0.
As soon as you move the weak-pointer management inside the class, there’s no way to tell it to invalidate the pointer before you’ve entered the destructor.

Maybe take a look as std::enable_shared_from_this https://en.cppreference.com/w/cpp/memory/enable_shared_from_this
if you want to be able to get a std::shared_ptr out of an existing object?

dave96:

But the problem remains, you can still end up in the situation where TestSafeObject is being destructed but the safeRef member is still valid so grants an owner_ that is in the middle of its destructor.

As the reset() call happens as the very first thing in the destructor, owner_ would only be at the beginning of its destructor rather than in the middle, without any destruction of it or its members yet to take place.

dave96:

But to be honest, the safest bet is to use std::shared_ptr and std::weak_ptr . They use atomic ref counts to ensure you can only dereference through a valid std::shared_ptr , even if obtained via a std::weak_ptr .
The only downside to this is that the memory storing the object will only be freed after all the weak_ptr s have been destroyed. This means you may need to poll them occasionally to reset them.

Your suggestion for using std::shared_ptr is good. But if I understand correctly, I cannot do that for my use-case externally to the object, because I’m implementing an existing API where I need to create a new object that is a sub-class of some given class… Perhaps I could used std::shared_ptr rather than juce::ReferenceCountedObject for my access-management, but it still has to be contained in my object iiuc…