Search code examples
c++c++11thread-safetyshared-ptr

shared_ptr and const methods thread-safety


Assume that we have a class A with const methods(only).

class A
{
public:
    void method1() const;
    void method2() const;
}

Also, we have another class B that has shared_ptr of class A and also methods that return it and change it with mutex protection:

class B
{
    std::shared_ptr<A> ptr;
    std::mutex m;
public:
    B()
    {
        ptr = std::make_shared<A>();
    }

    std::shared_ptr<A> getPtr() const
    {
        mutex_on_stack mut(m);
        return ptr;
    }

    void resetPtr()
    {
        mutex_on_stack mut(m);
        ptr.reset(new A());
    }
}

ptr variable is protected by mutexes, so I assume that it is thread-safe. But I am not sure about the safety of inner object itself.

B objB;

//thread 1
auto p1 = objB->getPtr();
p1->method1(); //calling const method

//thread 2
auto p2 = objB->getPtr();
p2->method2(); //calling const method

//thread 3
objB->resetPtr();

Threads 1 and 2 call getPtr() method and increase reference counter of shared_ptr as it copied. So I think that inner pointer will not be accidently deleted there by resetPtr() in thread 3. Also, methods method1 and method2 are const so it does not modify inner object. But I can be wrong.

So I have two questions:

  1. Is the code above thread-safe?
  2. Is the class A thread-safe at all?

Solution

  • Class A is thread safe because it only uses const functions, which may be assumed to not alter the externally visible state. While const functions are capable of modifying internal state if the class also has mutable members, it's common to assume any mutable members are synchronized so two concurrent readers of an object will never encounter race conditions.


    Accesses to the contents of a shared_ptr are thread safe, and in fact don't even require the additional synchronization of the mutex. From cppreference:

    All member functions (including copy constructor and copy assignment) can be called by multiple threads on different instances of shared_ptr without additional synchronization even if these instances are copies and share ownership of the same object.

    However, this only holds if you're not calling non-const functions of the shared_ptr in question:

    If multiple threads of execution access the same shared_ptr without synchronization and any of those accesses uses a non-const member function of shared_ptr then a data race will occur; the shared_ptr overloads of atomic functions can be used to prevent the data race.

    So class B wouldn't be quite thread-safe right now without the mutex, because you're calling reset on the function. It would be made thread-safe without the mutex if instead of calling reset you used the atomic overloads for shared_ptr.

    As it is, the only thing you need to do to ensure thread safety is ensure your RAII wrapper lasts until the end of the function scope, which it is doing.

    On the topic of the RAII wrapper, you don't need to roll your own lock-unlock mechanism. The standard library already supplies RAII locks for you, in the form of lock_guard, unique_lock and shared_lock. A simple scenario like this would probably match best with lock_guard, which is designed to function as a simple scoped lock.