Atomic Smart Pointers

Contents[Show]

C++20 will have atomic smart pointers. To be exact, 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 emphasize how important it is that the std::shared_ptr has well-defined multithreading semantics, and you know and use it. From the multithreading point of view, std::shared_ptr is this kind of data structure 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 behavior. On the other hand, there is a guideline in modern C++: Don't touch memory. That means, using smart pointers in multithreading programs.  

 

Rainer D 6 P2 540x540Modernes C++ Mentoring

Be part of my mentoring programs:

 

 

 

 

Do you want to stay informed about my mentoring programs: Subscribe via E-Mail.

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 is 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 assignment (2) is a race condition on the resource, and the program has undefined behavior.

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 specializations 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 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 ones for a non-atomic data type.

Correctness: Using free atomic operations is 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 behavior 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 significant advantage over the free atomic_* functions. They are designed for the particular use case multithreading and can have a std::atomic_flag as a kind of cheap Spinlock. (You can read the details about s 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 optimized for the particular use case.

For me, the correct argument is the most important 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 necessary to compile the program with a C++11 compiler are in red. The implementation of 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.

What's next?

C++11 got tasks like promises and futures an advanced multithreading concept. Although they offer a lot more threads, they have a significant shortcoming. C++11 futures can not be composed. Extended futures in C++20 will overcome this shortcoming. How? Read the next post.

 

 

 

 

Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Animus24, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Kris Kafka, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Matthieu Bolt, Stephen Kelley, Kyle Dean, Tusar Palauri, Dmitry Farberov, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, and Rob North.

 

Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, and Slavko Radman.

 

 

My special thanks to Embarcadero CBUIDER STUDIO FINAL ICONS 1024 Small

 

My special thanks to PVS-Studio PVC Logo

 

My special thanks to Tipi.build tipi.build logo

 

My special thanks to Take Up code TakeUpCode 450 60

 

Seminars

I'm happy to give online seminars or face-to-face seminars worldwide. Please call me if you have any questions.

Bookable (Online)

German

Standard Seminars (English/German)

Here is a compilation of my standard seminars. These seminars are only meant to give you a first orientation.

  • C++ - The Core Language
  • C++ - The Standard Library
  • C++ - Compact
  • C++11 and C++14
  • Concurrency with Modern C++
  • Design Pattern and Architectural Pattern with C++
  • Embedded Programming with Modern C++
  • Generic Programming (Templates) with C++

New

  • Clean Code with Modern C++
  • C++20

Contact Me

Modernes C++,

RainerGrimmDunkelBlauSmall

 

Comments   

+1 #1 Terra 2017-09-01 11:01
I truly value your work, Great post.
Quote
0 #2 Assaf Wodeslavsky 2017-09-25 04:26
I tested this code with c++11 version of the coce. I am using msvs2017.
Steps:
I pushed 20,000 nodes(int's).
I then ran 2 threads, each doing 'pop'. It crashed with Stack overflow.
I then started over.
I pushed 20,000 nodes(int's).
I then ran 2 threads, one doing 'push'. The other doing a 'pop'. It crashed with Stack overflow.
Here is my main:

int main()
{
const auto number_pushed = 10000;
concurrent_stack list;
auto push = [&]()
{
for (size_t i = 0; i < number_pushed; i++)
{
list.push_front(i);
}
};
auto pop = [&]()
{
for (size_t i = 0; i < number_pushed; i++)
{
list.pop_front();
}
};
{
thread push1(push);
thread push2(push);
push1.join();
push2.join();
}
//{
// // crashes
// thread pop1(pop);
// thread push1(push);
// pop1.join();
// push1.join();
//}
{
// crashes
thread pop1(pop);
thread pop2(pop);
pop1.join();
pop2.join();
}
return 0;
}
Quote
-4 #3 Rainer Grimm 2017-09-25 06:35
Quoting Assaf Wodeslavsky:
I tested this code with c++11 version of the coce. I am using msvs2017.
Steps:
I pushed 20,000 nodes(int's).
I then ran 2 threads, each doing 'pop'. It crashed with Stack overflow.
I then started over.
I pushed 20,000 nodes(int's).
I then ran 2 threads, one doing 'push'. The other doing a 'pop'. It crashed with Stack overflow.

I don't see your point. You used without synchronisation the non thread-safe data structure list. This is by definition a data-race. That means all results are possible.
Quote
0 #4 Sergey Markelov 2017-10-16 06:12
I could not compile the example using C++11 and red lines in the code until I changed the definition type of head to 'atomic(shared_ptr)'. Without my change I got the error "no matching function for call 'atomic_load'". Maybe you first red comment is not enough full.
Quote
0 #5 Rainer Grimm 2017-10-17 04:52
Quoting Sergey Markelov:
I could not compile the example using C++11 and red lines in the code until I changed the definition type of head to 'atomic(shared_ptr)'. Without my change I got the error "no matching function for call 'atomic_load'". Maybe you first red comment is not enough full.

Thanks. You are right. I cited this example from the proposal without changing.
Quote
+1 #6 rerdavies 2021-07-23 20:50
Maybe I'm missing something. It seem to me that a better way to describe the problem is that shared_ptr's are completely thread-safe, but references to shared_ptr's are not (so don't ever do that!). Would anyone reasonably expect them to be? The proposed use case of passing a reference to a shared pointer into a thread seems insanely obviously broken. Are there more compelling use cases for atomic references to shared_ptrs than that?
Quote

Stay Informed about my Mentoring

 

Mentoring

English Books

Course: Modern C++ Concurrency in Practice

Course: C++ Standard Library including C++14 & C++17

Course: Embedded Programming with Modern C++

Course: Generic Programming (Templates)

Course: C++ Fundamentals for Professionals

Course: The All-in-One Guide to C++20

Course: Master Software Design Patterns and Architecture in C++

Subscribe to the newsletter (+ pdf bundle)

All tags

Blog archive

Source Code

Visitors

Today 3704

Yesterday 4371

Week 39511

Month 169636

All 12057402

Currently are 155 guests and no members online

Kubik-Rubik Joomla! Extensions

Latest comments