C++20: Concurrency
This post concludes my overview of C++20. Today’s post is about the concurrency features in the next C++ standard.
Concurrency
C++20 has various concurrency improvements.
std::atomic_ref<T>
The class template std::atomic_ref applies atomic operations to the referenced non-atomic object. Concurrent writing and reading of the referenced object is no data race. The lifetime of the referenced object must exceed the lifetime of the atomic_ref. Accessing a subobject of the referenced object with an atomic_ref is not thread-safe.
Accordingly to std::atomic, std::atomic_ref can be specialized and supports specializations for the built-in data types.
struct Counters { int a; int b; }; Counter counter;
std::atomic_ref<Counters> cnt(counter);
std::atomic<std::shared_ptr<T>> and std::atomic<std::weak_ptr<T>>
std::shared_ptr is the only non-atomic data type on which you can apply atomic operations. First, let me write about the motivation for this exception. The C++ committee saw the necessity that instances of std::shared_ptr should provide a minimum atomicity guarantee in multithreading programs. What is this minimal atomicity guarantee for std::shared_ptr? The control block of the std::shared_ptr is thread-safe. This means that the increase and decrease operations of the reference counter are atomic. You also have the guarantee that the resource is destroyed exactly once.
Boost describes the assertions that a std::shared_ptr provides.
- A shared_ptr instance can be “read” (accessed using only constant operations) simultaneously by multiple threads.
- 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 exact reference count underneath).
With C++20 we get two new smart pointers: std::atomic<std::shared_ptr<T>> and std::atomic<std::weak_ptr<T>>.
Modernes C++ Mentoring
Do you want to stay informed: Subscribe.
Floating Point Atomics
In addition to C++11, you can create atomics for integral types and floating points. This is quite convenient when you have a floating-point number concurrently incremented by various threads. With a floating-point atomic, you don’t need to protect the floating pointer number.
Waiting on Atomics
std::atomic_flag is an atomic boolean. It has a clear and set state. For simplicity reasons, I call the clear state false and the set state true. Its clear method enables you to set its value to false. With the test_and_set method, you can set the value to true and return the previous value. There is no method to ask for the current value. This will change with C++20. With C++20, a std::atomic_flag has a test method.
Additionally, std::atomic_flag can be used for thread synchronization via the methods notify_one, notify_all, and wait. Notifying and waiting is with C++20 available on all partial and full specialization of std::atomic (bools, integrals, floats and pointers) and std::atomic_ref.
Semaphores, Latches, and Barriers
All three types are means to synchronize threads.
Semaphores
Semaphores are a synchronization mechanism that controls concurrent access to a shared resource. A counting semaphore, such as the one in C++20, is a special semaphore with a counter bigger than zero. The counter is initialized in the constructor. Acquiring the semaphore decreases the counter, and releasing the semaphore increases the counter. If a thread tries to acquire the semaphore when the counter is zero, the thread will block until another thread increments the counter by releasing the semaphore.
Latches and Barriers
Latches and barriers are simple thread synchronization mechanisms that enable some threads to block until a counter becomes zero.
What are the differences between these two mechanisms to synchronize threads? You can use a std::latch only once, but you can use a std::barrier more than once. A std::latch is useful for managing one task by multiple threads; a std::barrier is useful for managing repeated tasks by multiple threads. Additionally, a std::barrier can adjust the counter in each iteration.
The following code snippet is from Proposal N4204.
void DoWork(threadpool* pool) { latch completion_latch(NTASKS); // (1) for (int i = 0; i < NTASKS; ++i) { pool->add_task([&] { // (2) // perform work ... completion_latch.count_down();// (4) })}; // (3) } // Block until work is done completion_latch.wait(); // (5) }
The std::latch completion_latch is in its constructor set to NTASKS (line 1). The thread pool executes NTASKS (lines 2 – 3) jobs. At the end of each job (line 4), the counter is decremented. Line 5 is the barrier for the thread running the function DoWork and hence for the small workflow. This thread is blocked until all tasks have been finished.
std::jthread
std::jthread stands for joining thread. In addition to std::thread from C++11, std::jthread can automatically join the started thread and can be interrupted.
Here is the non-intuitive behaviour of std::thread. If a std::thread is still joinable, std::terminate is called in its destructor. A thread thr is joinable if either thr.join() nor thr.detach() was called.
// threadJoinable.cpp
#include <iostream> #include <thread> int main(){ std::cout << std::endl; std::cout << std::boolalpha; std::thread thr{[]{ std::cout << "Joinable std::thread" << std::endl; }}; std::cout << "thr.joinable(): " << thr.joinable() << std::endl; std::cout << std::endl; }
When executed, the program terminates.
Both executions of the program terminate. In the second run, the thread thr has enough time to display its message: “Joinable std::thread”.
In the next example, I replace the header <thread> with “jthread.hpp” and, therefore, use std::jthread from the upcoming C++20 standard.
// jthreadJoinable.cpp
#include <iostream> #include "jthread.hpp" int main(){ std::cout << std::endl; std::cout << std::boolalpha; std::jthread thr{[]{ std::cout << "Joinable std::jthread" << std::endl; }}; std::cout << "thr.joinable(): " << thr.joinable() << std::endl; std::cout << std::endl; }
Now, the thread thr automatically joins in its destructor such as in this case if still joinable.
What’s next?
I provided in the last four posts an overview of the new features in C++20. After the overview, let me dive into the details. My next post is about concepts.
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, Matt Godbolt, and Honey Sukesan.
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!