TimelineCpp20

Barriers and Atomic Smart Pointers in C++20

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 helps manage 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) 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 with the interface of a std::barrier bar.

 

Rainer D 6 P2 500x500Modernes C++ Mentoring

Be part of my mentoring programs:

  • "Fundamentals for C++ Professionals" (open)
  • "Design Patterns and Architectural Patterns with C++" (open)
  • "C++20: Get the Details" (open)
  • "Concurrency with Modern C++" (starts March 2024)
  • Do you want to stay informed: Subscribe.

     

    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, and 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, 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 on 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 given 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 behavior. On the other hand, there is a guideline in modern C++: Don’t use raw pointers. Consequently, 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 ones for a non-atomic data type.
    • Correctness: the usage of 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 significant advantage over the free atomic_* functions. The atomic versions are designed for the particular 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 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 required to compile the program with a C++11 compiler are marked in red. The implementation of 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.

    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, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, 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, Rob North, Bhavith C Achar, Marco Parri Empoli, moon, and Philipp Lenk.

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

    My special thanks to Embarcadero
    My special thanks to PVS-Studio
    My special thanks to Tipi.build 
    My special thanks to Take Up Code
    My special thanks to SHAVEDYAKS

    Seminars

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

    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++
    • Clean Code with Modern C++
    • C++20

    Online Seminars (German)

    Contact Me

    Modernes C++ Mentoring,

     

     

    0 replies

    Leave a Reply

    Want to join the discussion?
    Feel free to contribute!

    Leave a Reply

    Your email address will not be published. Required fields are marked *