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.
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:
- All threads are blocked.
- An arbitrary thread is unblocked and executes the callable.
- If the completion step is done, all threads are unblocked.
The following table presents you with the interface of a std::barrier bar.
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.
Modernes C++ Mentoring
Do you want to stay informed: Subscribe.
// 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.
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 ofstd::atomic_store(&ptr, localPt
r). 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 astd::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.
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, 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, Stephen Kelley, Kyle Dean, Tusar Palauri, 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, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery,and Matt Godbolt.
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 |
Modernes C++ GmbH
Modernes C++ Mentoring (English)
Rainer Grimm
Yalovastraße 20
72108 Rottenburg
Mail: schulung@ModernesCpp.de
Mentoring: www.ModernesCpp.org
Modernes C++ Mentoring,
Leave a Reply
Want to join the discussion?Feel free to contribute!