How can I create a smart pointer that locks and unlocks a mutex?

12,330

Solution 1

I'm not sure if there are any standard implementations, but since I like re-implementing stuff for no reason, here's a version that should work (assuming you don't want to be able to copy such pointers):

template<class T>
class locking_ptr
{
public:
  locking_ptr(T* ptr, mutex* lock)
    : m_ptr(ptr)
    , m_mutex(lock)
  {
    m_mutex->lock();
  }
  ~locking_ptr()
  {
    if (m_mutex)
      m_mutex->unlock();
  }
  locking_ptr(locking_ptr<T>&& ptr)
    : m_ptr(ptr.m_ptr)
    , m_mutex(ptr.m_mutex)
  {
    ptr.m_ptr = nullptr;
    ptr.m_mutex = nullptr;
  }

  T* operator ->()
  {
    return m_ptr;
  }
  T const* operator ->() const
  {
    return m_ptr;
  }
private:
  // disallow copy/assignment
  locking_ptr(locking_ptr<T> const& ptr)
  {
  }
  locking_ptr& operator = (locking_ptr<T> const& ptr)
  {
    return *this;
  }
  T* m_ptr;
  mutex* m_mutex; // whatever implementation you use
};

Solution 2

You're describing a variation of the EXECUTE AROUND POINTER pattern, described by Kevlin Henney in Executing Around Sequences.

I have a prototype implementation at exec_around.h but I can't guarantee it works correctly in all cases as it's a work in progress. It includes a function mutex_around which creates an object and wraps it in a smart pointer that locks and unlocks a mutex when accessed.

Solution 3

There is another approach here. Far less flexible and less generic, but also far simpler. While it still seems to fit your exact scenario.

shared_ptr (both standard and Boost) offers means to construct it while providing another shared_ptr instance which will be used for usage counter and some arbitrary pointer that will not be managed at all. On cppreference.com it is the 8th form (the aliasing constructor).

Now, normally, this form is used for conversions - like providing a shared_ptr to base class object from derived class object. They share ownership and usage counter but (in general) have two different pointer values of different types. This form is also used to provide a shared_ptr to a member value based on shared_ptr to object that it is a member of.

Here we can "abuse" the form to provide lock guard. Do it like this:

auto A::getResource()
{
    auto counter = std::make_shared<Lock>(&mMutex);
    std::shared_ptr<Resource> result{ counter, &mResource };
    return result;
}

The returned shared_ptr points to mResource and keeps mMutex locked for as long as it is used by anyone.

The problem with this solution is that it is now your responsibility to ensure that the mResource remains valid (in particular - it doesn't get destroyed) for that long as well. If locking mMutex is enough for that, then you are fine.

Otherwise, above solution must be adjusted to your particular needs. For example, you might want to have the counter a simple struct that keeps both the Lock and another shared_ptr to the A object owning the mResource.

Solution 4

To add to Adam Badura's answer, for a more general case using std::mutex and std::lock_guard, this worked for me:

auto A::getResource()
{
    auto counter = std::make_shared<std::lock_guard<std::mutex>>(mMutex);
    std::shared_ptr<Resource> ptr{ counter, &mResource} ;
    return ptr;
}

where the lifetimes of std::mutex mMutex and Resource mResource are managed by some class A.

Share:
12,330
Tim MB
Author by

Tim MB

Updated on June 07, 2022

Comments

  • Tim MB
    Tim MB almost 2 years

    I have a threaded class from which I would like to occasionally acquire a pointer an instance variable. I would like this access to be guarded by a mutex so that the thread is blocked from accessing this resource until the client is finished with its pointer.

    My initial approach to this is to return a pair of objects: one a pointer to the resource and one a shared_ptr to a lock object on the mutex. This shared_ptr holds the only reference to the lock object so the mutex should be unlocked when it goes out of scope. Something like this:

    void A::getResource()
    {
        Lock* lock = new Lock(&mMutex);
        return pair<Resource*, shared_ptr<Lock> >(
            &mResource, 
            shared_ptr<Lock>(lock));
    }
    

    This solution is less than ideal because it requires the client to hold onto the entire pair of objects. Behaviour like this breaks the thread safety:

    Resource* r = a.getResource().first;
    

    In addition, my own implementation of this is deadlocking and I'm having difficulty determining why, so there may be other things wrong with it.

    What I would like to have is a shared_ptr that contains the lock as an instance variable, binding it with the means to access the resource. This seems like something that should have an established design pattern but having done some research I'm surprised to find it quite hard to come across.

    My questions are:

    • Is there a common implementation of this pattern?
    • Are there issues with putting a mutex inside a shared_ptr that I'm overlooking that prevent this pattern from being widespread?
    • Is there a good reason not to implement my own shared_ptr class to implement this pattern?

    (NB I'm working on a codebase that uses Qt but unfortunately cannot use boost in this case. However, answers involving boost are still of general interest.)

  • stijn
    stijn about 11 years
    +1 this should do it - maybe disable assignment and add a move constructor?
  • riv
    riv about 11 years
    Sure, you could disable assignment by moving the operator = to private section, but it should be safe as long as mutexes support nested locks. Added move constructor.
  • riv
    riv about 11 years
    Fixed a couple typos; you could disable assignment with = delete keyword, but it is not supported by my Visual Studio.
  • Tom Knapen
    Tom Knapen about 11 years
    Why are you you assigning m_mutex = nullptr in your copy constructor? You don't do any sanity checking in your destructor so you will end up calling unlock() on a null pointer.
  • Christian Rau
    Christian Rau about 11 years
    Even in light of the danger of a possible off-topic discussion I don't really want to spark, but "I generally avoid using std libraries" - Ouch!
  • riv
    riv about 11 years
    Oops I copied the wrong code... And regarding standard libraries, not sure why I mentioned that, I just like having my own non-template-heavy versions in my personal code.
  • Jonathan Wakely
    Jonathan Wakely about 11 years
    Why have you implemented the copy constructor and copy assignment operator? Either make them private and unimplemented, or stop living in 1998 and define them as deleted.
  • riv
    riv about 11 years
    Good point about unimplemented; however, = delete is not supported in VC10/11.
  • Androvich
    Androvich over 2 years
    If using std::mutex, what type should Lock be?
  • Adam Badura
    Adam Badura over 2 years
    @Androvich, std::lock_guard as in your answer seems fine. You could use also std::unique_lock or any other implementation of the concept. However, for the presented need std::lock_guard is enough while it is also the simplest.
  • Androvich
    Androvich over 2 years
    Yeah, it's working perfectly. Also, in your answer, shouldn't it be std::make_shared<Lock>(mMutex) instead of std::make_shared<Lock>(&mMutex)?
  • Adam Badura
    Adam Badura over 2 years
    @Androvich, I don't think so. Lock will be a different type than mMutex.