Barriers and Atomic Smart Pointers in C++20

Contents[Show]

In my last post, I introduced latches in C++20. A latch enables its threads to wait until a counter becomes zero. Additionally, to a latch, its big sibling barrier can be used more than once. Today, I write about barriers and present atomic smart pointers.

 TimelineCpp20

If you are not familiar with std::latch, read my last post: Latches in C++20.

std::barrier

There are two differences between a std::latch and a std::barrier. A std::latch is useful for managing one task by multiple threads; a std::barrier is helpful for managing repeated tasks by multiple threads. Additionally, a std::barrier enables you to execute a function in the so-called completion step. The completion step is the state when the counter becomes zero. Immediately after the counter becomes zero, the so-called completion step starts. In this completion step, a callable is invoked. The std::barrier gets its callable in its constructor. A callable unit (short callable) is something that behaves like a function. Not only are these named functions, but also function objects or lambda expressions.

The completion step performs the following steps:

  1. All threads are blocked.
  2. An arbitrary thread is unblocked and executes the callable.
  3. If the completion step is done, all threads are unblocked.

The following table presents you the interface of a std::barrier bar.

barrier

 

The call bar.arrive_and_drop() call means essentially, that the counter is decremented by one for the next phase. The following program fullTimePartTimeWorkers.cpp halves the number of workers in the second phase.

// fullTimePartTimeWorkers.cpp

#include <iostream>
#include <barrier>
#include <mutex>
#include <string>
#include <thread>

std::barrier workDone(6);
std::mutex coutMutex;

void synchronizedOut(const std::string& s) noexcept {
    std::lock_guard<std::mutex> lo(coutMutex);
    std::cout << s;
}

class FullTimeWorker {                                                   // (1)
 public:
    FullTimeWorker(std::string n): name(n) { };
  
    void operator() () {
        synchronizedOut(name + ": " + "Morning work done!\n");
        workDone.arrive_and_wait();  // Wait until morning work is done     (3)
        synchronizedOut(name + ": " + "Afternoon work done!\n");
        workDone.arrive_and_wait();  // Wait until afternoon work is done   (4)
        
    }
 private:
    std::string name;
};
  
class PartTimeWorker {                                                   // (2)
 public:
    PartTimeWorker(std::string n): name(n) { };
  
    void operator() () {
        synchronizedOut(name + ": " + "Morning work done!\n");
        workDone.arrive_and_drop();  // Wait until morning work is done  // (5)
    }
 private:
    std::string name;
};

int main() {

    std::cout << '\n';

    FullTimeWorker herb("  Herb");
    std::thread herbWork(herb);
  
    FullTimeWorker scott("    Scott");
    std::thread scottWork(scott);
  
    FullTimeWorker bjarne("      Bjarne");
    std::thread bjarneWork(bjarne);
  
    PartTimeWorker andrei("        Andrei");
    std::thread andreiWork(andrei);
  
    PartTimeWorker andrew("          Andrew");
    std::thread andrewWork(andrew);
  
    PartTimeWorker david("            David");
    std::thread davidWork(david);

    herbWork.join();
    scottWork.join();
    bjarneWork.join();
    andreiWork.join();
    andrewWork.join();
    davidWork.join();
  
}

 

This workflow consists of two kinds of workers: full-time workers (1) and part-time workers (2). The part-time worker works in the morning, the full-time worker in the morning and the afternoon. Consequently, the full-time workers call workDone.arrive_and_wait() (lines (3) and (4)) two times. On the contrary, the part-time works call workDone.arrive_and_drop() (5) only once. This workDone.arrive_and_drop() call causes the part-time worker to skip the afternoon work. Accordingly, the counter has in the first phase (morning) the value 6, and in the second phase (afternoon) the value 3.

fullTimePartTimeWorkers

Now to something, I missed in my posts to atomics.

Atomic Smart Pointers

A std::shared_ptr consists of a control block and its resource. The control block is thread-safe, but access to the resource is not. This means modifying the reference counter is an atomic operation and you have the guarantee that the resource is deleted exactly once. These are the guarantees std::shared_ptr gives you.

On the contrary, it is crucial that a std::shared_ptr has well-defined multithreading semantics. At first glance, the use of a std::shared_ptr does not appear to be a sensible choice for multithreaded code. It is by definition shared and mutable and is the ideal candidate for non-synchronized read and write operations and hence for undefined behaviour. On the other hand, there is the guideline in modern C++: Don't use raw pointers. This means, consequently, that you should use smart pointers in multithreading programs when you want to model shared ownership.

The proposal N4162 for atomic smart pointers directly addresses the deficiencies of the current implementation. The deficiencies boil down to these three points: consistency, correctness, and performance.

  • Consistency: the atomic operations  std::shared_ptr are the only atomic operations for a non-atomic data type.
  • Correctness: the usage of the global atomic operations is quite error-prone because the correct usage is based on discipline. It is easy to forget to use an atomic operation - such as using ptr = localPtr instead of std::atomic_store(&ptr, localPtr). The result is undefined behaviour because of a data race. If we used an atomic smart pointer instead, the type system would not allow it.
  • Performance: the atomic smart pointers have a big advantage compared to the free atomic_* functions. The atomic versions are designed for the special use case and can internally have a std::atomic_flag as a kind of cheap spinlock. Designing the non-atomic versions of the pointer functions to be thread-safe would be overkill if they are used in a single-threaded scenario. They would have a performance penalty.

The correctness argument is probably 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 searching of elements. This singly linked list is implemented in a lock-free way.

AtomicSinglyLinkedList

 

All changes that are required to compile the program with a C++11 compiler are marked in red. The implementation with atomic smart pointers is a lot easier and hence less error-prone. C++20's type system does not permit it to use a non-atomic operation on an atomic smart pointer.

The proposal N4162 proposed the new types std::atomic_shared_ptr and std::atomic_weak_ptr as atomic smart pointers. By merging them in the mainline ISO C++ standard, they became partial template specialization of std::atomic: std::atomic<std::shared_ptr>, and std::atomic<std::weak_ptr>.

Consequently, the atomic operations for std::shared_ptr<T> are deprecated with C++20.

What's next?

With C++20, threads can be cooperatively interrupted.  Let me show you in my next, what that means.

 

Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, Marko, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Animus24, Jozo Leko, John Breland, espkk, Louis St-Amour, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Kris Kafka, Mario Luoni, Neil Wang, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Tobi Heideman, Daniel Hufschläger, Red Trip, Alexander Schwarz, Tornike Porchxidze, Alessandro Pezzato, Evangelos Denaxas, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Satish Vangipuram, and Michael Dunsky.

 

Thanks in particular to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, and Said Mert Turkal.

 

 

My special thanks to Embarcadero CBUIDER STUDIO FINAL ICONS 1024 Small

 

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.

New

Contact Me

Modernes C++,

RainerGrimmSmall

My Newest E-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

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

Subscribe to the newsletter (+ pdf bundle)

Blog archive

Source Code

Visitors

Today 4577

Yesterday 7071

Week 41553

Month 202227

All 6850919

Currently are 199 guests and no members online

Kubik-Rubik Joomla! Extensions

Latest comments