Latches and barriers are coordination types that enable some threads to wait until a counter becomes zero. You can use a std::latch
only once, but you can use a std::barrier
more than once. Today, I have a closer look at latches.
Concurrent invocations of the member functions of a std::latch
or a std::barrier
are no data race. A data race is such a crucial term in concurrency that I want to write more words to it.
Data Race
A data race is a situation, in which at least two threads access a shared variable at the same time and at least one thread tries to modify the variable. If your program has a data race, it has undefined behavior. This means all outcomes are possible and therefore, reasoning about the program makes no sense anymore.
Let me show you a program with a data race.
// addMoney.cpp #include <functional> #include <iostream> #include <thread> #include <vector> struct Account{ int balance{100}; // (3) }; void addMoney(Account& to, int amount){ // (2) to.balance += amount; // (1) } int main(){ std::cout << '\n'; Account account; std::vector<std::thread> vecThreads(100); for (auto& thr: vecThreads) thr = std::thread(addMoney, std::ref(account), 50); for (auto& thr: vecThreads) thr.join(); std::cout << "account.balance: " << account.balance << '\n'; // (4) std::cout << '\n'; }
100 threads adding 50 euros to the same account (1) using the function addMoney
(2). The initial account is 100 (3). The crucial observation is that the writing to the account is done without synchronization. Therefore we have a data race and, consequently, undefined behavior. The final balance is between 5000 and 5100 euro (4).
What is happening? Why are a few additions missing? The update process to.balance += amount;
in line (1) is a so-called read-modify-write operation. As such, first, the old value of to.balance
is read, then it is updated, and finally is written. What may happen under the hood is the following. I use numbers to make my argumentation more obvious
- Thread A reads the value 500 euro and then Thread B kicks in.
- Thread B read also the value 500 euro, adds 50 euro to it, and updates
to.balance
to 550 euro. - Now Thread A finished its execution by adding 50 euro to
to.balance
and also writes 550 euro. - Essential the value 550 euro is written twice and instead of two additions of 50 euro, we only observe one.
- This means, that one modification is lost and we get the wrong final sum.
First, there are two questions to answer before I present std::latch
and std::barrier
in detail.
Two Questions
- What is the difference between these two mechanisms to coordinate threads? You can use a
std::latch
only once, but you can use astd::barrier
more than once. Astd::latch
is useful for managing one task by multiple threads; astd::barrier
is helpful for managing repeated tasks by multiple threads. Additionally, astd::barrier
enables you to execute a function in the so-called completion step. The completion step is the state when the counter becomes zero. - What use cases do latches and barriers support that cannot be done in C++11 with futures, threads, or condition variables combined with locks? Latches and barriers address no new use cases, but they are a lot easier to use. They are also more performant because they often use a lock-free mechanism internally.
Let me continue my post with the simpler data type of both.
std::latch
Now, let us have a closer look at the interface of a std::latch
.
The default value for upd
is 1
. When upd
is greater than the counter or negative, the behavior is undefined. The call lat.try_wait()
does never wait as its name suggests.
The following program bossWorkers.cpp
uses two std::latch
to build a boss-workers workflow. I synchronized the output to std::cout
using the function synchronizedOut
(1). This synchronization makes it easier to follow the workflow.
// bossWorkers.cpp #include <iostream> #include <mutex> #include <latch> #include <thread> std::latch workDone(6); std::latch goHome(1); // (4) std::mutex coutMutex; void synchronizedOut(const std::string s) { // (1) std::lock_guard<std::mutex> lo(coutMutex); std::cout << s; } class Worker { public: Worker(std::string n): name(n) { }; void operator() (){ // notify the boss when work is done synchronizedOut(name + ": " + "Work done!\n"); workDone.count_down(); // (2) // waiting before going home goHome.wait(); // (5) synchronizedOut(name + ": " + "Good bye!\n"); } private: std::string name; }; int main() { std::cout << '\n'; std::cout << "BOSS: START WORKING! " << '\n'; Worker herb(" Herb"); std::thread herbWork(herb); Worker scott(" Scott"); std::thread scottWork(scott); Worker bjarne(" Bjarne"); std::thread bjarneWork(bjarne); Worker andrei(" Andrei"); std::thread andreiWork(andrei); Worker andrew(" Andrew"); std::thread andrewWork(andrew); Worker david(" David"); std::thread davidWork(david); workDone.wait(); // (3) std::cout << '\n'; goHome.count_down(); std::cout << "BOSS: GO HOME!" << '\n'; herbWork.join(); scottWork.join(); bjarneWork.join(); andreiWork.join(); andrewWork.join(); davidWork.join(); }
The idea of the workflow is straightforward. The six workers herb
, scott
, bjarne
, andrei
, andrew
, and david
in the main
-program have to fulfill their job. When they finished their job, they count down the std::latch workDone
(2). The boss (main
-thread) is blocked in line (3) until the counter becomes 0. When the counter is 0, the boss uses the second std::latch goHome
to signal its workers to go home. In this case, the initial counter is 1
(4). The call goHome.wait
(5) blocks until the counter becomes 0.
When you think about this workflow, you may notice that it can be performed without a boss. Here is the modern variant:
// workers.cpp #include <iostream> #include <latch> #include <mutex> #include <thread> std::latch workDone(6); std::mutex coutMutex; void synchronizedOut(const std::string& s) { std::lock_guard<std::mutex> lo(coutMutex); std::cout << s; } class Worker { public: Worker(std::string n): name(n) { }; void operator() () { synchronizedOut(name + ": " + "Work done!\n"); workDone.arrive_and_wait(); // wait until all work is done (1) synchronizedOut(name + ": " + "See you tomorrow!\n"); } private: std::string name; }; int main() { std::cout << '\n'; Worker herb(" Herb"); std::thread herbWork(herb); Worker scott(" Scott"); std::thread scottWork(scott); Worker bjarne(" Bjarne"); std::thread bjarneWork(bjarne); Worker andrei(" Andrei"); std::thread andreiWork(andrei); Worker andrew(" Andrew"); std::thread andrewWork(andrew); Worker david(" David"); std::thread davidWork(david); herbWork.join(); scottWork.join(); bjarneWork.join(); andreiWork.join(); andrewWork.join(); davidWork.join(); }
There is not much to add to this simplified workflow. The call workDone.arrive_and_wait(1)
(1) is equivalent to the calls count_down(upd); wait();
. This means the workers coordinate themself and the boss is no longer necessary such as in the previous program bossWorkers.cpp
.
What's next?
A std::barrier i
s quite similar to a std::latch
. std::barrier
's strength is it to perform a job more than once. In my next post, I will have a closer look at barriers.
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, Darshan Mody, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Animus24, Jozo Leko, John Breland, espkk, Wolfgang Gärtner, Louis St-Amour, Stephan Roslen, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Avi Kohn, Robert Blanch, Truels Wissneth, Kris Kafka, Mario Luoni, Neil Wang, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, and Peter Ware.
Thanks in particular to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, and Sudhakar Belagurusamy.
Seminars
I'm happy to give online-seminars or face-to-face seminars world-wide. Please call me if you have any questions.
Bookable (Online)
Deutsch
Standard Seminars
Here is a compilation of my standard seminars. These seminars are only meant to give you a first orientation.
New
Contact Me
Modernes C++,
from Hacker News https://ift.tt/2Y8JAd8
No comments:
Post a Comment
Note: Only a member of this blog may post a comment.