Download as docx, pdf, or txt
Download as docx, pdf, or txt
You are on page 1of 4

Atomic Smart Pointers

22 February 2017
Tweet
Contents[Show]
C++20 will have atomic smart pointers. To be exactly, we will get
a std::atomic_shared_ptr and a std::atomic_weak_ptr. But why? std::shared_ptr
and std::weak_ptr are already thread-safe. Sort of. Let me dive into the details.

Before I start, I want to make a short detour. This detour should only emphasise how import it is that
the std::shared_ptr has a well-defined multithreading semantic and you know and use it. From
the multithreading point of view, std::shared_ptr is this kind of data structures you will not use in
multithreading programs. They are by definition shared and mutable; therefore they are the ideal
candidates for data races and hence for undefined behaviour. At the other hand there is the
guideline in modern C++: Don't touch memory. That means, use smart pointers in multithreading
programs.

Half thread-safe
I often have the question in my C++ seminars: Are smart pointers thread-safe? My defined answer is
yes and no. Why? A std::shared_ptr consists of a control block and its resource. Yes, the
control block is thread-safe; but no, the access to the resource is not thread-safe. That means,
modifying the reference counter is an atomic operation and you have the guarantee that the
resource will be deleted exactly once. These are all guarantees a std::shared_ptr gives you.
The assertion that a std::shared_ptr provides, are described by Boost.

1. A shared_ptr instance can be "read" (accessed using only const operations)


simultaneously by multiple threads.
2. Different shared_ptr instances can be "written to" (accessed using mutable operations
such as operator= or reset) simultaneously by multiple threads (even when these instances
are copies, and share the same reference count underneath.)

To make the two statements clear, let me show a simple example. When you copy
a std::shared_ptr in a thread, all is fine.

std::shared_ptr<int> ptr = std::make_shared<int>(2011);

for (auto i= 0; i<10; i++){


std::thread([ptr]{ (1)
std::shared_ptr<int> localPtr(ptr); (2)
localPtr= std::make_shared<int>(2014); (3)
}).detach();
}
At first to (2). By using copy construction for the std::shared_ptr localPtr, only the control
block is used. That is thread-safe. (3) is a little bit more interesting. localPtr (3) is set to a
new std::shared_ptr.This is from the multithreading point of view no problem: Die lambda-
function (1) binds ptr by copy. Therefore, the modification of localPtr takes place on a copy.
The story will change dramatically if I take the std::shared_ptr by reference.

std::shared_ptr<int> ptr = std::make_shared<int>(2011);

for (auto i= 0; i<10; i++){


std::thread([&ptr]{ (1)
ptr= std::make_shared<int>(2014); (2)
}).detach();
}

The lambda-function binds the std::shared_ptr ptr by reference (1). Therefore the assignment
(2) is a race condition on the resource and the program has undefined behaviour.
Admittedly that was not so easy to get. std::shared_ptr requires special attention in a
multithreading environment. They are very special. They are the only non-atomic data types in C+
for which atomic operations exist.

Atomic Operations for std::shared_ptr


There are specialisations for the atomic operations load, store, compare and exchange for
a std::shared_ptr. By using the explicit variant you can even specify the memory
model. Here are the free atomic operations for std::shared_ptr.

std::atomic_is_lock_free(std::shared_ptr)
std::atomic_load(std::shared_ptr)
std::atomic_load_explicit(std::shared_ptr)
std::atomic_store(std::shared_ptr)
std::atomic_store_explicit(std::shared_ptr)
std::atomic_exchange(std::shared_ptr)
std::atomic_exchange_explicit(std::shared_ptr)
std::atomic_compare_exchange_weak(std::shared_ptr)
std::atomic_compare_exchange_strong(std::shared_ptr)
std::atomic_compare_exchange_weak_explicit(std::shared_ptr)
std::atomic_compare_exchange_strong_explicit(std::shared_ptr)

For the details, have a look at cppreference.com. Now it is quite easy to modify a by reference
bounded std::shared_ptr in a thread-safe way.

std::shared_ptr<int> ptr = std::make_shared<int>(2011);

for (auto i =0;i<10;i++){


std::thread([&ptr]{
auto localPtr= std::make_shared<int>(2014);
std::atomic_store(&ptr, localPtr); (1)
}).detach();
}

The update of the std::shared_ptr ptr (1) is thread-safe. All is well? NO. Finally, we come to
the new atomic smart pointers.

Atomic smart pointers


The proposal N4162 for atomic smart pointers directly addresses the deficiencies of the current
implementation. The deficiencies boil down to the three points consistency, correctness, and
performance. Here is an overview of the three points. For the details, you have to read the proposal.
Consistency: The atomic operations for the std::shared_ptr are the only atomic operations for
a non-atomic data type.
Correctness: The usage of the free atomic operations is quite error-prone because the right usage
is based on discipline. It's quite easy to forget to use an atomic operation - such as in the last
example: I use prt= localPtr instead of std::atomic_store(&ptr, localPtr). The result
is undefined behaviour because of a data race. If we have instead used an atomic smart pointer, the
compiler will not allow it.
Performance: The std::atomic_shared_ptr and std::atomic_weak_ptr have a big
advantage to the free atomic_* functions. They are designed for the special use case
multithreading and can have for example a std::atomic_flag as a kind of cheap Spinlock. (You
can read the details about spinlocks and std::atomic_flag in the post The Atomic Flag). It
makes of course not so much sense to put for a possible multithreading use cases
an std::atomic_flag in each std::shared_ptr or std::weak_ptr to make them thread-
safe. But that would be the consequence if both have a spinlock for the multithreading use case and
we would have no atomic smart pointers. That
means, std::shared_ptr and std::weak_ptr would have been optimised for the special use
case.
For me, the correctness argument is the most import one. Why? The answer lies in the proposal.
The proposal presents a thread-safe singly-linked list that supports insertion, deletion, and finding of
elements. This singly-linked list is implemented in a lock-free way.

A thread-safe singly-linked list

template<typename T> class concurrent_stack {


struct Node { T t; shared_ptr<Node> next; };
atomic_shared_ptr<Node> head;
// in C++11: remove “atomic_” and remember to use the special
// functions every time you touch the variable
concurrent_stack( concurrent_stack &) =delete;
void operator=(concurrent_stack&) =delete;

public:
concurrent_stack() =default;
~concurrent_stack() =default;
class reference {
shared_ptr<Node> p;
public:
reference(shared_ptr<Node> p_) : p{p_} { }
T& operator* () { return p->t; }
T* operator->() { return &p->t; }
};

auto find( T t ) const {


auto p = head.load(); // in C++11: atomic_load(&head)
while( p && p->t != t )
p = p->next;
return reference(move(p));
}
auto front() const {
return reference(head); // in C++11: atomic_load(&head)
}
void push_front( T t ) {
auto p = make_shared<Node>();
p->t = t;
p->next = head; // in C++11: atomic_load(&head)
while( !head.compare_exchange_weak(p->next, p) ){ }
// in C++11: atomic_compare_exchange_weak(&head, &p->next, p);
}
void pop_front() {
auto p = head.load();
while( p && !head.compare_exchange_weak(p, p->next) ){ }
// in C++11: atomic_compare_exchange_weak(&head, &p, p->next);
}
};

All changes that are necessary to compile the program with a C++11 compiler are red. The
implementation with atomic smart pointers is a lot easier and hence less error-prone. C++20 does
not permit it to use a non-atomic operation on a std::atomic_shared_ptr.

You might also like