Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

std::shared_ptr thread-safety

69 views
Skip to first unread message

Juha Nieminen

unread,
Apr 28, 2019, 1:28:24 PM4/28/19
to
The control block used by std::shared_ptr is thread-safe. This means
that if two different std::shared_ptr objects in two thread share
the same control block and both try to modify it (eg. its reference
count), no problems will happen.

std::shared_ptr *itself*, however, is not thread-safe.

This confuses me a bit. Exactly which member functions are safe to be
called from threads and in which situations?

Suppose we have a std::shared_ptr<Type> object named commonPtr somewhere,
visible to all threads. Am I correct to assume that doing this in
two different threads is safe?

std::shared_ptr<Type> localPtr = commonPtr;

while doing this is not:

commonPtr = localPtr;

Öö Tiib

unread,
Apr 28, 2019, 1:52:02 PM4/28/19
to
On Sunday, 28 April 2019 20:28:24 UTC+3, Juha Nieminen wrote:
> The control block used by std::shared_ptr is thread-safe. This means
> that if two different std::shared_ptr objects in two thread share
> the same control block and both try to modify it (eg. its reference
> count), no problems will happen.
>
> std::shared_ptr *itself*, however, is not thread-safe.
>
> This confuses me a bit. Exactly which member functions are safe to be
> called from threads and in which situations?

Same as with everything. If you want to write it from at least one
thread while accessing (does not matter if reading or writing) from
any other thread then you have race condition. So you have
to put locks around it.

> Suppose we have a std::shared_ptr<Type> object named commonPtr somewhere,
> visible to all threads. Am I correct to assume that doing this in
> two different threads is safe?
>
> std::shared_ptr<Type> localPtr = commonPtr;

Yes, assuming that no other thread is writing commonPtr.

> while doing this is not:
>
> commonPtr = localPtr;

Here thread is writing commonPtr so any other thread accessing it
is UB.

Paavo Helde

unread,
Apr 28, 2019, 2:44:17 PM4/28/19
to
On 28.04.2019 20:28, Juha Nieminen wrote:
> The control block used by std::shared_ptr is thread-safe. This means
> that if two different std::shared_ptr objects in two thread share
> the same control block and both try to modify it (eg. its reference
> count), no problems will happen.
>
> std::shared_ptr *itself*, however, is not thread-safe.
>
> This confuses me a bit. Exactly which member functions are safe to be
> called from threads and in which situations?

The potentially unsafe methods are the ones which change the pointer
value, i.e. assignment, swap, etc.

>
> Suppose we have a std::shared_ptr<Type> object named commonPtr somewhere,
> visible to all threads. Am I correct to assume that doing this in
> two different threads is safe?
>
> std::shared_ptr<Type> localPtr = commonPtr;

This line is safe as localPtr is either an automatic variable which is
not visible to other threads, or a namespace-level static whose
initialization is single-threaded.

> while doing this is not:
>
> commonPtr = localPtr;

Correct, if commonPtr is visible to multiple threads you will need some
kind of locking here.




Chris M. Thomasson

unread,
Apr 28, 2019, 7:38:43 PM4/28/19
to
Iirc, shared_ptr does not have strong thread safety. A thread needs to
own a reference before it can take another reference. So, something like
this will not work:

// global
static std::shared_ptr<Type> global_ptr;

// per-thread local

std::shared_ptr<Type> local_ptr = global_ptr;


The storing and loading from the global_ptr needs to be protected by a
lock. Now, there is something called atomic_ptr_plus:

http://atomic-ptr-plus.sourceforge.net/

Joe Seighs atomic_ptr can use double width atomic compare-and-swap, or
DWCAS, to get around this problem and not use an external lock. By the
way Juha, are you the same person that created:

http://paulbourke.net/fractals/povfrac/final/0007.pov

http://paulbourke.net/fractals/povfrac/final/

? Thanks.

Juha Nieminen

unread,
Apr 29, 2019, 2:41:40 AM4/29/19
to
Öö Tiib <oot...@hot.ee> wrote:
> Same as with everything. If you want to write it from at least one
> thread while accessing (does not matter if reading or writing) from
> any other thread then you have race condition. So you have
> to put locks around it.

"Write it"? Write what, exactly?

Paavo Helde

unread,
Apr 29, 2019, 6:53:21 AM4/29/19
to
On 29.04.2019 2:38, Chris M. Thomasson wrote:
> On 4/28/2019 10:28 AM, Juha Nieminen wrote:
>> The control block used by std::shared_ptr is thread-safe. This means
>> that if two different std::shared_ptr objects in two thread share
>> the same control block and both try to modify it (eg. its reference
>> count), no problems will happen.
>>
>> std::shared_ptr *itself*, however, is not thread-safe.
>>
>> This confuses me a bit. Exactly which member functions are safe to be
>> called from threads and in which situations?
>>
>> Suppose we have a std::shared_ptr<Type> object named commonPtr somewhere,
>> visible to all threads. Am I correct to assume that doing this in
>> two different threads is safe?
>>
>> std::shared_ptr<Type> localPtr = commonPtr;
>>
>> while doing this is not:
>>
>> commonPtr = localPtr;
>>
>
> Iirc, shared_ptr does not have strong thread safety. A thread needs to
> own a reference before it can take another reference. So, something like
> this will not work:
>
> // global
> static std::shared_ptr<Type> global_ptr;
>
> // per-thread local
>
> std::shared_ptr<Type> local_ptr = global_ptr;
>
>
> The storing and loading from the global_ptr needs to be protected by a
> lock.

Storing, sure. But loading from it should be safe if the shared_ptr
object stays unmodified, as is the case for other std:: classes like
std::string. And refcount change does not count as a modification here.

This is what the standard says about shared_ptr: "For purposes of
determining the presence of a data race, member functions shall access
and modify only the shared_ptr and weak_ptr objects themselves and not
objects they refer to. Changes in use_count() do not reflect
modifications that can introduce data races." ([util.smartptr.shared]).

So as far as no other thread calls any non-const member functions on
global_ptr, then reading it without locking should be safe. Please
correct me if I am wrong, as then I would need to fix this usage in my code.

If there is a chance another thread may change global_ptr concurrently,
then there would be indeed a possibility of a data race when reading it
and one has to e.g. wrap it inside a std::atomic or apply some external
locking (std::shared_ptr::atomic_load() will be deprecated in C++20 in
favor of std::atomic wrapping).



Chris M. Thomasson

unread,
Apr 29, 2019, 4:07:52 PM4/29/19
to
The problem is that the loading of the pointer and refcount increment
need to be a single atomic op in the internals of the ref count impl.
However, if the global_ptr was guaranteed to always point to the exact
same object, and was created and fully initialized _before_ any other
thread can access it, it might be okay. Humm... But, this is basically a
singleton?


> So as far as no other thread calls any non-const member functions on
> global_ptr, then reading it without locking should be safe. Please
> correct me if I am wrong, as then I would need to fix this usage in my
> code.

> If there is a chance another thread may change global_ptr concurrently,
> then there would be indeed a possibility of a data race when reading it
> and one has to e.g. wrap it inside a std::atomic or apply some external
> locking (std::shared_ptr::atomic_load() will be deprecated in C++20 in
> favor of std::atomic wrapping).

I am just not sure about the "special case" where the global_ptr is
completely setup before any threads can access it, and is guaranteed to
never change. But, why not use a singleton here?

Melzzzzz

unread,
Apr 29, 2019, 4:32:49 PM4/29/19
to
No. Shared ptr is always initialized in single thread.
Having global shared ptr beats the purpose of shared ptr. In that case
raw pointer would suffice.

Chris M. Thomasson

unread,
Apr 29, 2019, 5:25:57 PM4/29/19
to
Humm... Raw pointer doesn't suffice because it would not be reference
counted? Take a deep look at differential reference counting:

http://www.1024cores.net/home/lock-free-algorithms/object-life-time-management/differential-reference-counting

Dmitry, Joe and I have worked on this in the past over on
comp.programming.threads. Have you ever seen atomic_ptr by Joe Seigh?
Here is the patent:

https://patents.google.com/patent/US5295262

The idea is to have an atomic reference counted pointer that fully works
in the following scenario:
____________________________
static atomic_ptr<foo> g_foo = new foo();

// thread readers
for (;;)
{
local_ptr<foo> lfoo = g_foo;

// can test lfoo for a null object if needed

lfoo->read();
}

// thread writers
for (;;)
{
local_ptr<foo> next_foo = new foo();
g_foo = next_foo;
}
____________________________

This is strong thread safety and does not work with std::shared_ptr
as-is without some external sync.

Paavo Helde

unread,
Apr 29, 2019, 5:41:33 PM4/29/19
to
Yes, it might be a singleton. Singletons are needed sometimes. The
singleton itself would not need refcounting, but it might be needed to
pass it to interfaces which normally expect smartpointers to other,
shorter-lived objects. The singleton might play a role of a "fallback"
or "default" object in this scenario.

Another scenario is that the smartpointer is a non-changing member of
another shared object. The calling code copies the smartpointer member
for later use, no locking needed. Now the containing shared object can
be released safely. No singletons around.





Chris M. Thomasson

unread,
Apr 29, 2019, 7:19:21 PM4/29/19
to
Afaict, shared_ptr does not allow a thread to take a reference to an
object if it does not already own one. It can if external sync is used.
Not sure about the special case where shared_ptr will never change and
always points to the exact same address, forever.

Chris M. Thomasson

unread,
Apr 29, 2019, 7:19:59 PM4/29/19
to
Writing to global_ptr would be:

global_ptr = new foo();

Reading from it would be:

local_ptr = global_ptr.

Paavo Helde

unread,
Apr 30, 2019, 3:28:12 AM4/30/19
to
On 30.04.2019 2:19, Chris M. Thomasson wrote:
>
> Afaict, shared_ptr does not allow a thread to take a reference to an
> object if it does not already own one. It can if external sync is used.
> Not sure about the special case where shared_ptr will never change and
> always points to the exact same address, forever.
>

Here is a simple demo about what I had in mind. Note the lines (1) and
(2). One might argue that Wrap refcounting provides the needed external
sync here, but at least it is implicit and automatic.

#include <memory>
#include <iostream>
#include <thread>
#include <mutex>

// for serializing std::cout output only
std::mutex cout_mx;

struct A {
int x_;
A(int x) : x_(x) {}
~A() {
std::lock_guard<std::mutex> lock(cout_mx);
std::cout << "A destroyed\n";
}
void foo() {
std::lock_guard<std::mutex> lock(cout_mx);
std::cout << "I am A (x=" << x_ << ") in thread " <<
std::this_thread::get_id() << "\n";
}
};

struct Wrap {
const std::shared_ptr<A> a_; // (0) a_ is non-mutable
Wrap(int x) : a_(std::make_shared<A>(x)) {}
~Wrap() {
std::lock_guard<std::mutex> lock(cout_mx);
std::cout << "Wrap destroyed\n";
}
};

void OtherThread(std::shared_ptr<Wrap> wrap) {
auto a = wrap->a_; // (1) no locking
wrap.reset();
a->foo();
}

int main() {
auto wrap = std::make_shared<Wrap>(42);
std::thread t1(OtherThread, wrap);
auto a = wrap->a_; // (2) no locking
wrap.reset();
a->foo();
a.reset();
t1.join();
}


A possible output:

I am A (x=42) in thread 2216
Wrap destroyed
I am A (x=42) in thread 12512
A destroyed

Juha Nieminen

unread,
Apr 30, 2019, 6:58:12 AM4/30/19
to
Chris M. Thomasson <invalid_chris_t...@invalid.com> wrote:
> The problem is that the loading of the pointer and refcount increment
> need to be a single atomic op in the internals of the ref count impl.

It is my understanding that the control block used by std::shared_ptr
which it creates when it starts managing a new object (ie. the piece
of memory it allocates for bookkeeping) is fully thread-safe as per
the standard.

Which ought to mean that two separate std::shared_ptr instances that are
pointing to the same object (ie. sharing it) can freely modify eg. the
reference count for that object at the same time without problems
(which happens if they are eg. assigned to another std::shared_ptr
instance, or go out of scope, or whatever), without the programmer
having to take care of it.

However, the std::shared_ptr class *itself* is not thread-safe as-is
(and to make it thread-safe you need to use eg.
std::atomic<std::shared_ptr<T>>).

It's just a bit confusing exactly which operations, and in which situations,
are thread-safe as-is, and which are not. Some things can be done safely,
others need explicit locking (eg. by using std::atomic in C++20).

Öö Tiib

unread,
Apr 30, 2019, 7:01:04 AM4/30/19
to
Write to the shared_ptr itself. It is internally designed as a
pointer. That pointer potentially points at control block (accesses
of what are synchronized). That block is either allocated together
with or potentially points at actual data (accesses of what are
again not synchronized). With "write it" I meant that pointer.

Bonita Montero

unread,
Apr 30, 2019, 8:09:34 AM4/30/19
to
> Write to the shared_ptr itself. It is internally designed as a
> pointer. That pointer potentially points at control block (accesses
> of what are synchronized). That block is either allocated together
> with or potentially points at actual data (accesses of what are
> again not synchronized). With "write it" I meant that pointer.

The control-block shouldn't have the pointer to the data.
All shared_ptr's themselfes should have two pointers, one to the
control-block and one to the data. Having a concatenated pointer
to the control-block and then to the data itself would disable
out-of-order-execution, i.e. operations on the data would be
preceded by two in-order loads.

Paavo Helde

unread,
Apr 30, 2019, 8:54:13 AM4/30/19
to
On 30.04.2019 13:58, Juha Nieminen wrote:
> Chris M. Thomasson <invalid_chris_t...@invalid.com> wrote:
>> The problem is that the loading of the pointer and refcount increment
>> need to be a single atomic op in the internals of the ref count impl.
>
> It is my understanding that the control block used by std::shared_ptr
> which it creates when it starts managing a new object (ie. the piece
> of memory it allocates for bookkeeping) is fully thread-safe as per
> the standard.
>
> Which ought to mean that two separate std::shared_ptr instances that are
> pointing to the same object (ie. sharing it) can freely modify eg. the
> reference count for that object at the same time without problems
> (which happens if they are eg. assigned to another std::shared_ptr
> instance, or go out of scope, or whatever), without the programmer
> having to take care of it.

Exactly. The programmer does not need to know or care about internal
implementation details like the control block or reference counting.

> However, the std::shared_ptr class *itself* is not thread-safe as-is
> (and to make it thread-safe you need to use eg.
> std::atomic<std::shared_ptr<T>>).

Right, shared_ptr does not contain synchronization for accessing itself
because it would be both counter-productive (for pointers visible in a
single thread only), and impossible to achieve (for shared pointers).

E.g. consider a global shared shared_ptr:

std::shared_ptr<A> common_ptr = ...;

thread 1:
common_ptr->foo();

thread 2:
common_ptr = std::make_shared<A>();

Even if shared_ptr had internal locking and ensured that the pointer
gets replaced atomically and with proper memory barriers in thread 2,
this would still not guarantee that the pointed object is not released
and destroyed in the middle of foo() in thread 1.

To guarantee the proper lifetime, the operator->() should return some
kind of proxy object, which would hold the object alive until foo() is
completed. However, the standard says it returns a plain pointer, so
this cannot be done.

In short, shared_ptr with the current interface cannot protect against
its own modifications (not to speak about its own destruction which
could not be protected against anyway) and so external synchronization
is needed for modifications.

Note that refcounter sync is much easier and incurs less penalties, for
example thread 1 above does not access the refcounter at all.

>
> It's just a bit confusing exactly which operations, and in which situations,
> are thread-safe as-is, and which are not. Some things can be done safely,
> others need explicit locking (eg. by using std::atomic in C++20).

In my mind this is clear, it is safe to call any number of const member
functions in parallel, but as soon as there appears a non-const member
function call in some thread, all threads must use external sync. Chris
seems to agree, he just does not see much point in having a smartpointer
which cannot be reassigned or reset. He is probably right.

Anyway, all this discussion is a bit academic because shared_ptr
mechanism is meant for sharing the pointed object, not sharing the
shared_ptr itself. For passing shared_ptr pointers to other threads
there are usually better ways than to set up a global shared_ptr somewhere.

Chris M. Thomasson

unread,
Apr 30, 2019, 3:20:46 PM4/30/19
to
On 4/30/2019 5:54 AM, Paavo Helde wrote:
> On 30.04.2019 13:58, Juha Nieminen wrote:
>> Chris M. Thomasson <invalid_chris_t...@invalid.com> wrote:
>>> The problem is that the loading of the pointer and refcount increment
>>> need to be a single atomic op in the internals of the ref count impl.
>>
>> It is my understanding that the control block used by std::shared_ptr
>> which it creates when it starts managing a new object (ie. the piece
>> of memory it allocates for bookkeeping) is fully thread-safe as per
>> the standard.
[...]

In order to achieve strong thread safety wrt a reference counted
pointer, it needs to ensure that the reference count is incremented
along with the load of the pointer in a single atomic operation.

This does not work with shared_ptr:
________________________
static shared_ptr<foo> global = new foo(); // before any threads...

// readers
shared_ptr<foo> local = global; // READ
local->foo();

// writers
shared_ptr<foo> local = new foo();
global = local; // WRITE
________________________


> Anyway, all this discussion is a bit academic because shared_ptr
> mechanism is meant for sharing the pointed object, not sharing the
> shared_ptr itself. For passing shared_ptr pointers to other threads
> there are usually better ways than to set up a global shared_ptr somewhere.
>

shared_ptr only has basic thread safety. Joe Seigh created atomic_ptr
with strong thread safety. This is basically my only point. shared_ptr
cannot act like atomic_ptr without some external sync.

Chris M. Thomasson

unread,
Apr 30, 2019, 3:31:47 PM4/30/19
to
On 4/30/2019 5:54 AM, Paavo Helde wrote:
> On 30.04.2019 13:58, Juha Nieminen wrote:
>> Chris M. Thomasson <invalid_chris_t...@invalid.com> wrote:
>>> The problem is that the loading of the pointer and refcount increment
>>> need to be a single atomic op in the internals of the ref count impl.
>>
>> It is my understanding that the control block used by std::shared_ptr
>> which it creates when it starts managing a new object (ie. the piece
>> of memory it allocates for bookkeeping) is fully thread-safe as per
>> the standard.
[...]

In the strong thread safety case, one needs to load a pointer and
increment the reference count in a single atomic operation. shared_ptr
by itself does not do that. Now, I am wondering if
std::atomic<shared_ptr<foo>> can declare itself as lock free, via:

https://en.cppreference.com/w/cpp/atomic/atomic/is_lock_free

If so, it would be using something like Joe's atomic_ptr. Differential
reference counting is key here.

Öö Tiib

unread,
May 1, 2019, 3:22:33 AM5/1/19
to
Yes, I mixed it up. Thanks for pointing it out.
0 new messages