Synchronization with Atomics in C++20
Sender/receiver workflows are pretty common for threads. In such a workflow, the receiver is waiting for the sender’s notification before it continues to work. There are various ways to implement these workflows. With C++11, you can use condition variables or promise/future pairs; with C++20, you can use atomics.
There are various ways to synchronize threads. Each way has its pros and cons. Consequently, I want to compare them. I assume you don’t know the details of condition variables or promises and futures. Therefore, I will give a short refresher.
Condition Variables
A condition variable can fulfill the role of a sender or a receiver. As a sender, it can notify one or more receivers.
// threadSynchronisationConditionVariable.cpp #include <iostream> #include <condition_variable> #include <mutex> #include <thread> #include <vector> std::mutex mutex_; std::condition_variable condVar; std::vector<int> myVec{}; void prepareWork() { // (1) { std::lock_guard<std::mutex> lck(mutex_); myVec.insert(myVec.end(), {0, 1, 0, 3}); // (3) } std::cout << "Sender: Data prepared." << std::endl; condVar.notify_one(); } void completeWork() { // (2) std::cout << "Worker: Waiting for data." << std::endl; std::unique_lock<std::mutex> lck(mutex_); condVar.wait(lck, [] { return not myVec.empty(); }); myVec[2] = 2; // (4) std::cout << "Waiter: Complete the work." << std::endl; for (auto i: myVec) std::cout << i << " "; std::cout << std::endl; } int main() { std::cout << std::endl; std::thread t1(prepareWork); std::thread t2(completeWork); t1.join(); t2.join(); std::cout << std::endl; }
The program has two child threads:t1
and t2
. They get their payload prepareWork
and completeWork
in lines (1) and (2). The function prepareWork
notifies that it is done with the preparation of the work: condVar.notify_one()
. While holding the lock, the thread t2
is waiting for its notification: condVar.wait(lck, []{ return not myVec.empty(); })
. The waiting thread always performs the same steps. When it is waked up, it checks the predicate while holding the lock ([]{ return not myVec.empty();
). If the predicate does not hold, it puts itself back to sleep. If the predicate holds, it continues with its work. In the concrete workflow, the sending thread puts the initial values into the std::vector
(3), which the receiving thread completes (4).
Modernes C++ Mentoring
Do you want to stay informed: Subscribe.
Condition variables have many inherent issues. For example, the receiver could be awakened without notification or could lose the notification. The first issue is spurious wakeup, and the second is lost wakeup. The predicate protects against both flaws. The notification would be lost when the sender sends its notification before the receiver is in the wait state and does not use a predicate.
Consequently, the receiver waits for something that never happens. This is a deadlock. When you study the program’s output, you see that each second run would cause a deadlock if I would not use a predicate. Of course, it is possible to use condition variables without a predicate.
If you want to know the sender/receiver workflow details and the traps of condition variables, read my previous post, “C++ Core Guidelines: Be Aware of the Traps of Condition Variables“.
When you only need a one-time notification, such as in the previous program, promises and futures are a better choice than condition variables. Promises and futures cannot be victims of spurious or lost wakeups.
Promises and Futures
A promise can send a value, an exception, or a notification to its associated future. Let me use a promise and a future to refactor the previous workflow. Here is the same workflow using a promise/future pair.
// threadSynchronisationPromiseFuture.cpp #include <iostream> #include <future> #include <thread> #include <vector> std::vector<int> myVec{}; void prepareWork(std::promise<void> prom) { myVec.insert(myVec.end(), {0, 1, 0, 3}); std::cout << "Sender: Data prepared." << std::endl; prom.set_value(); // (1) } void completeWork(std::future<void> fut){ std::cout << "Worker: Waiting for data." << std::endl; fut.wait(); // (2) myVec[2] = 2; std::cout << "Waiter: Complete the work." << std::endl; for (auto i: myVec) std::cout << i << " "; std::cout << std::endl; } int main() { std::cout << std::endl; std::promise<void> sendNotification; auto waitForNotification = sendNotification.get_future(); std::thread t1(prepareWork, std::move(sendNotification)); std::thread t2(completeWork, std::move(waitForNotification)); t1.join(); t2.join(); std::cout << std::endl; }
When you study the workflow, you recognize that the synchronization is reduced to its essential parts: prom.set_value()
(1) and fut.wait() (2).
There is neither a need to use locks or mutexes nor is there a need to use a predicate to protect against spurious or lost wakeups. I skip the screenshot to this run because it is essentially the same, such as in the case of the previous run with condition variables.
One downside to using promises and futures is that they can only be used once. Here are my previous posts on promises and futures, often called tasks.
If you want to communicate more than once, use condition variables or atomics.
std::atomic_flag
std::atomic_flag in C++11 has a simple interface. Its member function clear lets you set its value to false, with test_and_set to true. In case you use test_and_set you get the old value back. ATOMIC_FLAG_INIT
enables it to initialize the std::atomic_flag
to false
. std::atomic_flag has two exciting properties.
std::atomic_flag is
- the only lock-free atomic.
- the building block for higher thread abstractions.
The remaining more powerful atomics can provide their functionality by using a mutex. That is according to the C++ standard. So these atomics have a member function is_lock_free .On the popular platforms, I always get the answer true
. But you should be aware of that. Here are more details on the capabilities of std::atomic_flag
C++11.
Now, I jump directly from C++11 to C++20. With C++20, std::atomic_flag
atomicFlag
support new member functions: atomicFlag.wait(
), atomicFlag.notify_one()
, and atomicFlag.notify_all()
. The member functions notify_one
or notify_all
notify one or all of the waiting atomic flags. atomicFlag.wait(boo)
needs a boolean boo
. The call atomicFlag.wait(boo)
blocks until the subsequent notification or spurious wakeup. It checks then if the value atomicFlag
is equal to boo
and unblocks if not. The value boo
serves as a kind of predicate.
Additionally, to C++11, default construction of a std::atomic_flag
sets it in its false
state, and you can ask for the value of the std::atomic flag
via atomicFlag.test()
. With this knowledge, it’s pretty easy to refactor previous programs using a std::atomic_flag
.
// threadSynchronisationAtomicFlag.cpp #include <iostream> #include <atomic> #include <thread> #include <vector> std::vector<int> myVec{}; std::atomic_flag atomicFlag{}; void prepareWork() { myVec.insert(myVec.end(), {0, 1, 0, 3}); std::cout << "Sender: Data prepared." << std::endl; atomicFlag.test_and_set(); // (1) atomicFlag.notify_one(); } void completeWork() { std::cout << "Worker: Waiting for data." << std::endl; atomicFlag.wait(false); // (2) myVec[2] = 2; std::cout << "Waiter: Complete the work." << std::endl; for (auto i: myVec) std::cout << i << " "; std::cout << std::endl; } int main() { std::cout << std::endl; std::thread t1(prepareWork); std::thread t2(completeWork); t1.join(); t2.join(); std::cout << std::endl; }
The thread preparing the work (1) sets the atomicFlag
to true
and sends the notification. The thread completing the work waits for the notification. It is only unblocked if atomicFlag
is equal to true
.
Here are a few runs of the program with the Microsoft Compiler.
I’m not sure if I would use a future/promise pair or a std::atomic_flag
for such a simple thread synchronization workflow. Both are thread-safe by design and require no protection mechanism so far. Promise and promise are easier to use but std::atomic_flag
is probably faster. I’m sure I would not use a condition variable if possible.
What’s next?
When you create a more complicated thread synchronization workflow, such as a ping/pong game, a promise/future pair is no option. You have to use condition variables or atomics for multiple synchronizations. In my next post, I will implement a ping/pong game using condition variables and a std::atomic_flag
and measure their performance.
Short Break
I take a short Christmas break and publish the following post on the 11.th of January. To learn more about C++20, read my new book at Leanpub to C++20.
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!