24

I'm trying to understand exactly how thread-safe, atomic reference counting works, for example as with std::shared_ptr. I mean, the basic concept is simple, but I'm really confused about how the decref plus delete avoids race conditions.

This tutorial from Boost demonstrates how an atomic thread-safe reference counting system can be implemented using the Boost atomic library (or the C++11 atomic library).

#include <boost/intrusive_ptr.hpp>
#include <boost/atomic.hpp>

class X {
public:
  typedef boost::intrusive_ptr<X> pointer;
  X() : refcount_(0) {}

private:
  mutable boost::atomic<int> refcount_;
  friend void intrusive_ptr_add_ref(const X * x)
  {
    x->refcount_.fetch_add(1, boost::memory_order_relaxed);
  }
  friend void intrusive_ptr_release(const X * x)
  {
    if (x->refcount_.fetch_sub(1, boost::memory_order_release) == 1) {
      boost::atomic_thread_fence(boost::memory_order_acquire);
      delete x;
    }
  }
};

Okay, so I get the general idea. But I don't understand why the following scenario is NOT possible:

Say the refcount is currently 1.

  1. Thread A: atomically decrefs the refcount to 0.
  2. Thread B: atomically increfs the refcount to 1.
  3. Thread A: calls delete on the managed object pointer.
  4. Thread B: sees the refcount as 1, accesses the managed object pointer... SEGFAULT!

I can't understand what prevents this scenario from occurring, since there is nothing preventing a data race from between the time the refcount reaches 0, and the object is deleted. Decrefing the refcount and calling delete are two separate, non-atomic operations. So how is this possible without a lock?

curiousguy
  • 8,038
  • 2
  • 40
  • 58
Siler
  • 8,976
  • 11
  • 64
  • 124
  • 5
    How does the refcount is 1 while there are two references from the two threads? – Curve25519 Jul 06 '15 at 20:22
  • You need to spell out the invariant of a use count. A use count should never reach 0 while there are users. Your code must have a bug somewhere. – curiousguy Jan 19 '17 at 23:49

5 Answers5

30

You probably overestimate the threadsafety a shared_ptr provides.

The essence of atomic ref counting is to ensure that if two different instances of a shared_ptr (that are managing the same object) are accessed/modified, there will be no race condition. However, shared_ptr doesn't ensure thread safety, if two threads access the same shared_ptr object (and one of them is a write). One example would be e.g. if one thread dereferences the pointer, while the other resets it.
So about the only thing shared_ptr gurantees is that there will be no double delete and no leak as long as there is no race on a single instance of a shared_ptr (It also doesn't make accesses to the object it points to threadsafe)

As a result, also creating a copy of a shared_ptr is only safe, if there is no other thread that could delete/reset it at the same time (you could also say, it is not internally synchronized). This is the scenario you describe.

To repeat it once more: Accessing a single shared_ptr instance from multiple threads where one of those accesses is a write to the pointer is still a race condition.

If you want to e.g. copy a std::shared_ptrin a threadsafe manner, you have to ensure that all loads and stores happen via std::atomic_... operations which are specialized for shared_ptr.

MikeMB
  • 20,029
  • 9
  • 57
  • 102
  • 2
    Atomic operations on `shared_ptr` are little known and underrated. +1 – sehe Jul 06 '15 at 23:00
  • @sehe: A more accurate description is that "atomic operations on shared pointers are horribly misdesigned and broken" :-) Efforts are underway to improve that situation, though. – Kerrek SB Jul 07 '15 at 09:44
  • @sehe: I have to admit that I haven't used them myself, yet. I only remembered them from Herb Sutter's talk about lock-free programming at cppcon2014. – MikeMB Jul 07 '15 at 10:13
  • I used them. But they aren't widely supported yet. Also, the standard doesn't require the implementation to actually use atomics. – sehe Jul 07 '15 at 10:17
  • Does this also apply to QSharedPointer? Qt documentation states that "QSharedPointer and QWeakPointer are thread-safe and operate atomically on the pointer value. " – philipp Aug 10 '15 at 12:21
  • @philipp: As you said yourself, all methods of QSharedPointer are threadsafe. So the above answer doesn't apply to it, but only to std::shared_ptr (and I believe also to the boost version). Actually, my impression is that most things in QT work differently than in the STL. – MikeMB Aug 10 '15 at 16:20
  • For the record, the presentation at ccpcon2014, mentioned by @MikeMB, can be found at https://github.com/CppCon/CppCon2014/blob/master/Presentations/Lock-Free%20Programming%20%28or%2C%20Juggling%20Razor%20Blades%29/Lock-Free%20Programming%20%28or%2C%20Juggling%20Razor%20Blades%29%20-%20Herb%20Sutter%20-%20CppCon%202014.pdf But it does not directly answer this point, AFAIK. – Martin Quinson Apr 26 '17 at 10:36
  • @MartinQuinson: I only mentioned it as the reason, why I was aware of those specializations at all (although I still haven't used them). What point exactly would you would you like to have answered? Maybe I can oblige – MikeMB Apr 26 '17 at 13:36
  • I'm currently fighting against a similar bug, and found the presentation interesting (even if not perfect), thus the posting as comment. – Martin Quinson Apr 26 '17 at 14:59
3

Your scenario is not possible because Thread B should have been created with an incremented refcount already. Thread B should not be incrementing the ref count as the first thing it does.

Let's say Thread A spawns Thread B. Thread A has the responsibility to increment the ref count of the object BEFORE creating the thread, to guarantee thread safety. Thread B then only has to call release when it exits.

If Thread A creates Thread B without incrementing the ref count, bad things might happen as you've described.

Paladine
  • 513
  • 3
  • 11
3

Thread B: atomically increfs the refcount to 1.

Impossible. To increment the reference count to one, the reference count would have to be zero. But if the reference count is zero, how is thread B accessing the object at all?

Either thread B has a reference to the object or it doesn't. If it does, then the reference count cannot be zero. If it does not, then why is it messing with an object managed by smart pointers when it has no reference to that object?

David Schwartz
  • 179,497
  • 17
  • 214
  • 278
3

The implementation doesn't provide or require such a guarantee, avoidance of the behavior you are describing is predicated on the proper management of counted-references, usually done through a RAII class such as std::shared_ptr. The key is to entirely avoid passing by raw pointer across scopes. Any function which stores or retains a pointer to the object must take a shared pointer so that it can properly increment the ref count.

void f(shared_ptr p) {
   x(p); // pass as a shared ptr
   y(p.get()); // pass raw pointer
}

This function was passed a shared_ptr so the refcount was already 1+. Our local instance, p, should have bumped the ref_count during copy-assignment. When we called x if we passed by value we created another ref. If we passed by const ref, we retained our current ref count. If we passed by non-const ref then it's feasible that x() released the reference and y is going to be called with null.

If x() stores/retains the raw pointer, then we may have a problem. When our function returns the refcount might reach 0 and the object might be destroyed. This is our fault for not correctly maintaining ref count.

Consider:

template<typename T>
void test()
{
    shared_ptr<T> p;
    {
        shared_ptr<T> q(new T); // rc:1
        p = q; // rc:2
    } // ~q -> rc:1
    use(p.get()); // valid
} // ~p -> rc:0 -> delete

vs

template<typename T>
void test()
{
    T* p;
    {
        shared_ptr<T> q(new T); // rc:1
        p = q; // rc:1
    } // ~q -> rc:0 -> delete
    use(p); // bad: accessing deleted object
}
kfsone
  • 23,617
  • 2
  • 42
  • 74
  • Still, this can be a problem by just using the shared pointers assignment operator. I came into such a situation I described here: http://stackoverflow.com/questions/31918932/what-is-the-cost-of-calling-member-function-via-shared-pointer – philipp Aug 10 '15 at 12:33
1

For std::shared_ptr a reference counting change is thread safe, but not the access to the content of the `shared_ptr.

Regarding boost::intrusive_ptr<X>, this is no answer.