c++ why std::shared_ptr need atomic_store, atomic_load or why we need atomic shared_ptr

shared_ptr is some sort of confusing especially why we need atomic_store, atomic_load on that.

It is NOT thread safe.

In fact,  the primary intended use of std::shared_ptr is letting multiple threads control the lifetime of the same object, but not thread-safe.

A good summary is:

  • shared_ptr behaves like a built-in type regarding thread-safety,
  • Concurrent access to distinct objects is fine, while concurrent access to the same object is not.

That is, multiple threads may manipulate different shared_ptrs pointing to the same object as they like. but if they access the shared_ptr instance ( using non-const function like reset(),=) ,  then it is not safe, we need to use atomic_store/load. Think of a instance of shared_ptr just like any other instance of obj.

  • Of course if we need to access its real pointed to obj’s non-const member functions, those member function need synchronization ( mutex etc) protection.

one good reference is:

http://www.modernescpp.com/index.php/atomic-smart-pointers

 

A better way to understand it is by example, use cases:

Use case 1:     access the same std::shared_ptr object from different threads

A code modified version from :

https://wandbox.org/permlink/oAXbYnkKE5BR7Wig

to demostrate the atomic_store/load is needed

 

#include <iostream>
#include <memory>
#include <thread>
#define loop_times 10

std::shared_ptr<int> g;

void read_g()
{
std::shared_ptr<int> x;
long sum = 0;
for (int i = 0; i < loop_times; ++i)
{
//x = g;     // crash version
x= atomic_load(&g);  // good version, we need atomic here

std::cout << “x is: ” << *x << “, use_count: ” << x.use_count() << std::endl;
sum += *x;
x.reset();
}
printf(“sum = %ld\n”, sum);
}

void write_g()
{

for (int i = 0; i < loop_times; ++i)
{
auto n = std::make_shared<int>(i);
//g = n;   //crash version
atomic_store(&g,n);   // good version, we need atomic here
std::cout << “g is: ” << *g << “, use_count: ” << g.use_count() << std::endl;
}
}

int main()
{
g = std::make_shared<int>(0);
std::thread t1(read_g);
std::thread t2(write_g);
t1.join();
t2.join();
}

That reason being https://en.cppreference.com/w/cpp/memory/shared_ptr/atomic said:

If multiple threads of execution access the same std::shared_ptr object without synchronization

and any of those accesses uses a non-const member function of shared_ptr

then a data race will occur unless all such access is performed through these functions, which are overloads of the corresponding atomic access functions (std::atomic_loadstd::atomic_store, etc.)

also from https://stackoverflow.com/questions/14482830/stdshared-ptr-thread-safety:

shared_ptr<> is a mechanism to ensure that multiple object owners ensure an object is destructed,

not a mechanism to ensure multiple threads can access an object correctly.

You still need a separate synchronization mechanism to use it safely in multiple threads (like std::mutex).

https://en.cppreference.com/w/cpp/memory/shared_ptr said:

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. 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.

In other word: if I have  p = make_shared<A>(0); p1 = p; p2=p;

now in thread 1, I can do something on p1, like: p1.reset(), p1=make_share<A>(1)

and thread 2: I can do something on p2, like: p2.reset(), p2= make_share<A>(2)

as p, p1, p2, are difference instances, that is thread safe in the sense of munipulate shared_ptr itself ( using shared_ptr’s member functions),

but  calling p2->fun1(), p1->fun1() is not thread-safe ( here it invovle de-reference the point and invoke real object’s function).

 

Use Case 2:  Access the real data ( of those shared_ptr) from multiple thread is not safe. we need synchroniztion to call those methods:

// here the shared_ptr is mainly letting multiple threads control the lifetime of the same object, but if access shared_ptr obj’s data from multple threads

// we need mutex etc.

class A {

public:

A( int id ): id_(id) {};   ~A(){ std::cout << “A ” << id_ << ” destroyed.\n”; };

void set_id(int t) {    // need mutex here to make it safe to be accessed from multiple threads

id_ = t ;

};
int id() const { return id_; };

private: int id_;

};

auto  a=make_shared<A>(1)

auto a2=a;

// thread 1:

auto th1=std::thread([&](){
std::shared_ptr<AnyClass> p = a;   // this is thread safe, as copying a shared_ptr only mutates the control block, and not the shared_ptr itself.

p->set_id(1);   // access data is not safe, as it is not const member fn, it will update data itself
});

// thread 2:

auto th2=std::thread([&](){

std::shared_ptr<AnyClass> p = a; // this is thread safe, as copying a shared_ptr only mutates the control block, and not the shared_ptr itself.

p->set_id(2); // access data is not safe, as it is not const member fn, it will update data itself
});
});

we actually need to synchronize on the set_id() in order for this to be thread safe.

 

 Use case 3: letting multiple threads control the lifetime of the same object, only do READ access only

// thread 1:

auto th1=std::thread([&](){
std::shared_ptr<AnyClass> p = a;   // this is thread safe, as copying a shared_ptr only mutates the control block, and not the shared_ptr itself.

p->id();   // // access data is ok if we only read it.
});

// thread 2:

auto th2=std::thread([&](){

std::shared_ptr<AnyClass> p = a; // this is thread safe, as copying a shared_ptr only mutates the control block, and not the shared_ptr itself.

p->id(); // access data is ok if we only read it.
});
});

 

Implementation notes from https://en.cppreference.com/w/cpp/memory/shared_ptr

In a typical implementation, std::shared_ptr holds only two pointers:

  • the stored pointer (one returned by get());
  • a pointer to control block.

The control block is a dynamically-allocated object that holds:

  • either a pointer to the managed object or the managed object itself;
  • the deleter (type-erased);
  • the allocator (type-erased);
  • the number of shared_ptrs that own the managed object;
  • the number of weak_ptrs that refer to the managed object.

When shared_ptr is created by calling std::make_shared or std::allocate_shared, the memory for both the control block and the managed object is created with a single allocation. The managed object is constructed in-place in a data member of the control block. When shared_ptr is created via one of the shared_ptr constructors, the managed object and the control block must be allocated separately. In this case, the control block stores a pointer to the managed object.

The pointer held by the shared_ptr directly is the one returned by get(), while the pointer/object held by the control block is the one that will be deleted when the number of shared owners reaches zero. These pointers are not necessarily equal.

The destructor of shared_ptr decrements the number of shared owners of the control block. If that counter reaches zero, the control block calls the destructor of the managed object. The control block does not deallocate itself until the std::weak_ptr counter reaches zero as well.

In existing implementations, the number of weak pointers is incremented ([1][2]) if there is a shared pointer to the same control block.

To satisfy thread safety requirements, the reference counters are typically incremented using an equivalent of std::atomic::fetch_add with std::memory_order_relaxed (decrementing requires stronger ordering to safely destroy the control block).

 

The above block of notes should reveal us that why access to the same shared_ptr instance from multiple threads is not safe.

As we could image that shared_ptr copy constructor/assignment constructor/reset() etc need multiple steps of operations to finish

( imaging it need munipulate two pointers) , thus it will not atomic. thus not thread-safe at all.

So atomic_store/load will come to rescue this, ( internally it could add mutex-protected, or spin-lock or whatever means to make it atomic?)

 

References:

https://stackoverflow.com/questions/9200664/how-is-the-stdtr1shared-ptr-implemented

http://www.openguru.com/2015/07/a-simple-sharedptr-implementation-in-c.html

https://herbsutter.com/2013/06/05/gotw-91-solution-smart-pointer-parameters/

https://shaharmike.com/cpp/shared-ptr/

https://www.reddit.com/r/cpp/comments/5do55l/heres_everything_i_know_about_shared_ptr/

https://stackoverflow.com/questions/40223599/what-is-the-difference-between-stdshared-ptr-and-stdexperimentalatomic-sha

https://www.quora.com/How-is-thread-safe-shared_ptr-in-its-core-Is-it-ok-to-just-synchronize-its-internal-pointer-when-working-in-multiple-threads

 

Please rate this


Leave a Reply

Your email address will not be published. Required fields are marked *


*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>