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.