ラッチとバリアは、一部のスレッドがカウンターがゼロになるまで待機できるようにする調整タイプです。 std::latch
を使用できます 一度だけですが、 std::barrier
を使用できます 一回以上。今日は、ラッチを詳しく見ていきます。
std::latch
のメンバー関数の同時呼び出し または std::barrier
はデータ競合ではありません。データ競合は並行性において非常に重要な用語であるため、もっと多くの言葉を書きたいと思います.
データ競争
データ競合とは、少なくとも 2 つのスレッドが同時に共有変数にアクセスし、少なくとも 1 つのスレッドが変数を変更しようとする状況です。プログラムにデータ競合がある場合、その動作は未定義です。これは、すべての結果が可能であることを意味し、したがって、プログラムについての推論はもはや意味がありません.
データ競合のあるプログラムをお見せしましょう。
// 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'; }
関数 addMoney
を使用して、同じアカウントに 50 ユーロを追加する 100 スレッド (1) (2)。初期アカウントは 100 (3) です。重要な観察は、アカウントへの書き込みが同期なしで行われることです。したがって、データ競合が発生し、その結果、未定義の動作が発生します。最終的な残高は 5000 から 5100 ユーロ (4) です。
何が起こっている?いくつかの追加が欠落しているのはなぜですか?更新プロセス to.balance += amount;
行 (1) は、いわゆる read-modify-write 操作です。そのため、最初に to.balance
の古い値 読み取られ、更新され、最後に書き込まれます。ボンネットの下で何が起こるかは次のとおりです。議論をより明確にするために数字を使用します
- スレッド A が 500 ユーロの値を読み取り、スレッド B が起動します。
- スレッド B も値 500 ユーロを読み取り、それに 50 ユーロを追加して、
to.balance
を更新します。 550 ユーロまで。 - スレッド A は
to.balance
に 50 ユーロを追加して実行を終了しました 550 ユーロも書いています。 - 重要なのは、550 ユーロの値が 2 回書かれていることです。50 ユーロが 2 回追加されるのではなく、1 回だけ確認されます。
- これは、1 つの変更が失われ、間違った最終的な金額が得られることを意味します。
まず、std::latch
を提示する前に 2 つの質問に答えてください。 および std::barrier
2 つの質問
<オール> std::latch
を使用できます 一度だけですが、 std::barrier
を使用できます 一回以上。 std::latch
複数のスレッドで 1 つのタスクを管理するのに役立ちます。 std::barrier
複数のスレッドで繰り返されるタスクを管理するのに役立ちます。さらに、std::barrier
いわゆる完了ステップで機能を実行できます。完了ステップは、カウンターがゼロになったときの状態です。両方の単純なデータ型について投稿を続けましょう。
std::latch
では、std::latch
のインターフェースを詳しく見てみましょう。 .
upd
のデフォルト値 1
です . upd
のとき がカウンタより大きいか負の場合、動作は未定義です。呼び出し lat.try_wait()
その名前が示すように、決して待ちません。
次のプログラム bossWorkers.cpp
2 つの std::latch
を使用 上司と従業員のワークフローを構築します。出力を std::cout
に同期しました 関数 synchronizedOut
を使用します (1)。この同期により、ワークフローに従うことが容易になります。
// 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(); }
ワークフローの考え方は簡単です。 6人の労働者 herb
、 scott
、 bjarne
、 andrei
、 andrew
、および david
main
で -プログラムはその仕事を果たさなければなりません。彼らは仕事を終えると、std::latch workDone
をカウントダウンします。 (2)。ボス (main
-thread) はカウンターが 0 になるまで行 (3) でブロックされます。カウンターが 0 になると、ボスは 2 番目の std::latch goHome
を使用します 従業員に帰宅するように合図する。この場合、初期カウンターは 1
です (4)。呼び出し goHome.wait
(5) カウンターが0になるまでブロックする。
このワークフローについて考えると、ボスがいなくても実行できることに気付くかもしれません。これが最新のバリアントです:
// 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(); }
この簡素化されたワークフローに追加することはあまりありません。呼び出し workDone.arrive_and_wait(1)
(1) count_down(upd); wait();
の呼び出しと同等 .これは、以前のプログラム bossWorkers.cpp
のように、労働者が自分自身を調整し、ボスが不要になったことを意味します。 .
次は?
std::barrier
std::latch
によく似ています . std::barrier
の強みは、仕事を複数回行うことです。次回の投稿では、障壁について詳しく見ていきます。