bobeff bobeff - 1 month ago 25
C++ Question

What is the difference between std::shared_ptr and std::experimental::atomic_shared_ptr?

I read the following article by Antony Williams and as I understood in addition to the atomic shared count in

std::shared_ptr
in
std::experimental::atomic_shared_ptr
the actual pointer to the shared object is also atomic?

But when I read about reference counted version of
lock_free_stack
described in Antony's book about C++ Concurrency it seems for me that the same aplies also for
std::shared_ptr
, because functions like
std::atomic_load
,
std::atomic_compare_exchnage_weak
are applied to the instances of
std::shared_ptr
.

template <class T>
class lock_free_stack
{
public:
void push(const T& data)
{
const std::shared_ptr<node> new_node = std::make_shared<node>(data);
new_node->next = std::atomic_load(&head_);
while (!std::atomic_compare_exchange_weak(&head_, &new_node->next, new_node));
}

std::shared_ptr<T> pop()
{
std::shared_ptr<node> old_head = std::atomic_load(&head_);
while(old_head &&
!std::atomic_compare_exchange_weak(&head_, &old_head, old_head->next));
return old_head ? old_head->data : std::shared_ptr<T>();
}

private:
struct node
{
std::shared_ptr<T> data;
std::shared_ptr<node> next;

node(const T& data_) : data(std::make_shared<T>(data_)) {}
};

private:
std::shared_ptr<node> head_;
};


What is the exact difference between this two types of smart pointers, and if pointer in
std::shared_ptr
instance is not atomic, why it is possible the above lock free stack implementation?

Answer

The atomic "thing" in shared_ptr is not the shared pointer itself, but the control block it points to. meaning that as long as you don't mutate the shared_ptr across multiple threads, you are ok. do note that coying a shared_ptr only mutates the control block, and not the shared_ptr itself.

std::shared_ptr<int> ptr = std::make_shared<int>(4);
for (auto i =0;i<10;i++){
   std::thread([ptr]{ auto copy = ptr; }).detach(); //ok, only mutates the control block 
}

but mutating the shared pointer itself, such as assigning it different values from multiple threads, is a data race, for example:

std::shared_ptr<int> ptr = std::make_shared<int>(4);
std::thread threadA([&ptr]{
   ptr = std::make_shared<int>(10);
});
std::thread threadB([&ptr]{
   ptr = std::make_shared<int>(20);
});    

so here, we are mutating the control block(which is ok) but also the shared pointer itself, by making it point to a different values from multiple threads. this is not ok.

a solution to that problem is to wrap the shared_ptr with a lock. but this solution is not so scalable under some contention, and in a sense, loose the automatic feeling of the standard shared pointer.

another solution is to use the standard functions you quoted, as std::atomic_compare_exchange_weak. this makes the work of synchronizing shared pointers a manual one, which we don't like.

This is where atomic shared pointer comes to play. you can mutate the shared pointer from multiple threads without fearing of data race and without using any locks. the standalone functions will be members ones, and their use will be much more natural for the user. this kind of pointer is extremely useful for lock free data structure.

Comments