Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

std::atomic load method decreases the reference count when used with std::shared_ptr

I would like to use a std::atomic<std::shared_ptr> in my code so that the shared_ptr can be atomicaly updated, but I have a problem when accessing the shared_ptr. The load() method on the atomic seems to reduce the ref-count on the shared_ptr, so that I can't actually use the object without it being deallocated.

Here is a simplified piece of code that shows the problem...

typedef shared_ptr<MyClass> MyClassPtr;
typedef atomic<MyClassPtr> MyClassAtomicPtr;

// 1.
MyClassPtr ptr( new MyClass() );
printf("1. use_count=%d\n", ptr.use_count());

// 2. 
MyClassAtomicPtr atomicPointer(ptr);
printf("2. use_count=%d\n", ptr.use_count());

// 3.
{
    MyClassPtr p = atomicPointer.load();
    printf("3a. use_count=%d\n", ptr.use_count());
}
printf("3b. use_count=%d\n", ptr.use_count());

// 4.
{
    MyClassPtr p = atomicPointer.load();
    printf("4a. use_count=%d\n", ptr.use_count());
}
printf("4b. use_count=%d\n", ptr.use_count());

The output of this is:

1. use_count=1
2. use_count=2
3a. use_count=2
3b. use_count=1
4a. use_count=1
4b. use_count=-572662307

I understand steps 1 and 2. But at step 3, I would expect the assignment to the shared_ptr to increase the ref-count to 3, and then when it goes out of scope for the ref-count to go back down to 2. But in fact it stays at 2 when assigned and then decreases to 1 when the shared_ptr goes out of scope. Similarly in step 4, where the ref-count goes to zero and the object is deleted.

So my question is: how can I access and use the shared_ptr managed by the atomic without destroying it?

(I was compiling with Visual Studio 2012 Version 11.0.50727.1 RTMREL)

like image 479
Richard Shepherd Avatar asked Nov 04 '12 08:11

Richard Shepherd


People also ask

What happens when you move shared_ptr?

By moving the shared_ptr instead of copying it, we "steal" the atomic reference count and we nullify the other shared_ptr . "stealing" the reference count is not atomic, and it is hundred times faster than copying the shared_ptr (and causing atomic reference increment or decrement).

What is a role of the reference counter in shared_ptr?

The shared reference counter counts the number of owners. Copying a std::shared_ptr increases the reference count by one. Destroying a std::shared_ptr decreases the reference count by one. If the reference count becomes zero, the resource will automatically be released.


3 Answers

You can't use std::shared_ptr<T> as template argument type for std::atomic<T>. "The type of the template argument T shall be trivially copyable." (§29.5 1 in N3290) std::shared_ptr<T> is not trivially copyable.

Apparently, in your example std::memcpy (or something like that) is used to copy the std::shared_ptr and afterwards the destructor is invoked. That's the reason for the decrementing reference count. In the last step, the object is deleted.

A solution is to use a std::mutex to protect your std::shared_ptr.

like image 123
nosid Avatar answered Oct 24 '22 17:10

nosid


I believe the standard way to atomically load and store shared pointers are to use the functions in §20.7.2.5[util.smartptr.shared.atomic]. It seems only libc++ of clang support them:

template<class T> bool atomic_is_lock_free(const shared_ptr<T>* p);
template<class T> shared_ptr<T> atomic_load(const shared_ptr<T>* p);
template<class T> shared_ptr<T> atomic_load_explicit(const shared_ptr<T>* p, memory_order mo);
template<class T> void atomic_store(shared_ptr<T>* p, shared_ptr<T> r);
template<class T> void atomic_store_explicit(shared_ptr<T>* p, shared_ptr<T> r, memory_order mo);
template<class T> shared_ptr<T> atomic_exchange(shared_ptr<T>* p, shared_ptr<T> r);
template<class T> shared_ptr<T> atomic_exchange_explicit(shared_ptr<T>* p, shared_ptr<T> r, memory_order mo);
template<class T> bool atomic_compare_exchange_weak(shared_ptr<T>* p, shared_ptr<T>* v, shared_ptr<T> w);
template<class T> bool atomic_compare_exchange_strong(shared_ptr<T>* p, shared_ptr<T>* v, shared_ptr<T> w);
template<class T> bool atomic_compare_exchange_weak_explicit(shared_ptr<T>* p, shared_ptr<T>* v, shared_ptr<T> w, memory_order success, memory_order failure);
template<class T> bool atomic_compare_exchange_strong_explicit(shared_ptr<T>* p, shared_ptr<T>* v, shared_ptr<T> w, memory_order success, memory_order failure);

So you code could be written as:

auto ptr = std::make_shared<MyClass>();
printf("1. use_count=%d\n", ptr.use_count());

{
    auto p = std::atomic_load(&ptr);
    printf("3a. use_count=%d\n", ptr.use_count());
}

printf("3b. use_count=%d\n", ptr.use_count());

{
    auto p = std::atomic_load(&ptr);
    printf("3a. use_count=%d\n", ptr.use_count());
}

printf("4b. use_count=%d\n", ptr.use_count());

But I can't find such supports listed on MSDN, so the best you could do is to use a mutex. (Actually, the implementation of these functions in libc++ uses a mutex too.)

like image 42
kennytm Avatar answered Oct 24 '22 17:10

kennytm


Down in the guts of the implementation, the std::atomic ctor you're calling must be assigning its internal pointer with something like:

std::atomic(T* ctorInput) {
   memcpy(myPtr, ctorInput, sizeof(T));
}

What this means is that it doing a direct copy on the bytes, bypassing any real copy constructor "T(const T&)". That's why it only works correctly on a 'trivially copyable' type, namely one whose copy constructor doesn't do anything anyway. Since shared_ptr DOES do real work, namely an atomic increment, in its copy ctor that work isn't getting done by std::atomic because it never invokes the call. You then get a mysterious off-by-1 error in the reference count.

like image 38
user3726672 Avatar answered Oct 24 '22 17:10

user3726672