競合状態とデータ競合は関連していますが、概念は異なります。それらは関連しているため、しばしば混同されます。ドイツ語では、kritischer Wettlauf という用語で両方の表現を翻訳しています .正直言って、それはとても悪いことです。並行性について推論するには、言葉遣いが正確でなければなりません。したがって、この投稿は競合状態とデータ競合に関するものです。
最初に、ソフトウェアの領域で両方の用語を定義させてください。
- 競合状態: 競合状態とは、操作の結果が特定の個々の操作のインターリーブに依存する状況です。
- データ競合 :データ競合とは、少なくとも 2 つのスレッドが同時に共有変数にアクセスする状況です。少なくとも 1 つのスレッドが変数を変更しようとしています。
競合状態自体は悪くありません。競合状態は、データ競合の原因になる可能性があります。逆に、データ競合は未定義の動作です。したがって、あなたのプログラムに関するすべての推論は、もはや意味をなしません。
良性ではないさまざまな種類の競合状態を紹介する前に、競合状態とデータ競合を含むプログラムを示したいと思います。
競合状態とデータ競合
競合状態とデータ競合の典型的な例は、あるアカウントから別のアカウントに送金する機能です。シングルスレッドの場合、すべて問題ありません。
シングルスレッド
// account.cpp #include <iostream> struct Account{ // 1 int balance{100}; }; void transferMoney(int amount, Account& from, Account& to){ if (from.balance >= amount){ // 2 from.balance -= amount; to.balance += amount; } } int main(){ std::cout << std::endl; Account account1; Account account2; transferMoney(50, account1, account2); // 3 transferMoney(130, account2, account1); std::cout << "account1.balance: " << account1.balance << std::endl; std::cout << "account2.balance: " << account2.balance << std::endl; std::cout << std::endl; }
私の主張を明確にするために、ワークフローは非常に単純です。各アカウントは 100 $ (1) の残高から始まります。お金を引き出すには、口座に十分なお金がなければなりません (2)。十分な金額が利用可能な場合、その金額は最初に古いアカウントから削除され、次に新しいアカウントに追加されます。 2 回の送金が行われます (3)。アカウント 1 からアカウント 2 へ、およびその逆です。 transferMoney の各呼び出しは、次々に発生します。それらは、全体的な順序を確立する一種のトランザクションです。それは結構です。
両方の口座の残高は良さそうです。
実際には、transferMoney は同時に実行されます。
マルチスレッド
いいえ、データ競合と競合状態があります。
// accountThread.cpp #include <functional> #include <iostream> #include <thread> struct Account{ int balance{100}; }; // 2 void transferMoney(int amount, Account& from, Account& to){ using namespace std::chrono_literals; if (from.balance >= amount){ from.balance -= amount; std::this_thread::sleep_for(1ns); // 3 to.balance += amount; } } int main(){ std::cout << std::endl; Account account1; Account account2; // 1 std::thread thr1(transferMoney, 50, std::ref(account1), std::ref(account2)); std::thread thr2(transferMoney, 130, std::ref(account2), std::ref(account1)); thr1.join(); thr2.join(); std::cout << "account1.balance: " << account1.balance << std::endl; std::cout << "account2.balance: " << account2.balance << std::endl; std::cout << std::endl; }
transferMoney の呼び出しは同時に実行されます (1)。スレッドによって実行される関数への引数は、値によって移動またはコピーする必要があります。 account1 や account2 などの参照をスレッド関数に渡す必要がある場合は、std::ref などの参照ラッパーでラップする必要があります。スレッド t1 と t2 のために、関数 transferMoney (2) で口座の残高にデータ競合があります。しかし、競合状態はどこにありますか?競合状態を可視化するために、スレッドを短時間スリープさせます (3)。式 std::this_thread::sleep_for(1ns) の組み込みリテラル 1ns は、ナノ秒を表します。この投稿では、Raw と Cooked は新しい組み込みリテラルの詳細です。 C++14 以降、一定期間使用しています。
ところで。多くの場合、並行プログラムでの短いスリープ期間で、問題を可視化するのに十分です。
これがプログラムの出力です。
そして、あなたが見る。最初の関数 transferMoney のみが実行されました。バランスが小さすぎたため、2つ目は実行されませんでした。その理由は、最初の送金が完了する前に 2 回目の出金が行われたためです。ここに競合状態があります。
データ競合を解決するのは非常に簡単です。天びんの操作は保護する必要があります。アトミック変数でやった.
// accountThreadAtomic.cpp #include <atomic> #include <functional> #include <iostream> #include <thread> struct Account{ std::atomic<int> balance{100}; }; void transferMoney(int amount, Account& from, Account& to){ using namespace std::chrono_literals; if (from.balance >= amount){ from.balance -= amount; std::this_thread::sleep_for(1ns); to.balance += amount; } } int main(){ std::cout << std::endl; Account account1; Account account2; std::thread thr1(transferMoney, 50, std::ref(account1), std::ref(account2)); std::thread thr2(transferMoney, 130, std::ref(account2), std::ref(account1)); thr1.join(); thr2.join(); std::cout << "account1.balance: " << account1.balance << std::endl; std::cout << "account2.balance: " << account2.balance << std::endl; std::cout << std::endl; }
もちろん、アトミック変数は競合状態を解決しません。データ競争だけがなくなりました。
次は?
データ競合と競合状態を持つ誤ったプログラムを提示しただけです。しかし、悪意のある競合状態にはさまざまな側面があります。不変条件の解除、デッドロックやライブロックなどのロックの問題、または切り離されたスレッドの存続期間の問題。競合状態のないデッドロックもあります。次の投稿では、競合状態の悪意のある影響について書きます。