マルチスレッド アプリケーションを作成するときに発生する最も一般的な問題の 1 つは、競合状態です。
コミュニティへの私の質問は次のとおりです。
- レースコンディションとは?
- それらをどのように検出しますか?
- それらをどのように扱いますか?
- 最後に、それらの発生をどのように防止しますか?
マルチスレッド アプリケーションを作成するときに発生する最も一般的な問題の 1 つは、競合状態です。
コミュニティへの私の質問は次のとおりです。
競合状態は、2 つ以上のスレッドが共有データにアクセスでき、同時にそのデータを変更しようとすると発生します。スレッド スケジューリング アルゴリズムはいつでもスレッド間でスワップできるため、スレッドが共有データにアクセスしようとする順序はわかりません。したがって、データの変更の結果は、スレッドのスケジューリング アルゴリズムに依存します。つまり、両方のスレッドがデータへのアクセス/変更を「競合」しています。
問題は、1 つのスレッドが「check-then-act」(たとえば、値が X である場合は「check」し、値が X であることに依存する何かを行うために「act」) を実行し、別のスレッドが値に対して何かを実行するときに発生することがよくあります。 「確認」と「行為」の間。例えば:
if (x == 5) // The "Check"
{
y = x * 2; // The "Act"
// If another thread changed x in between "if (x == 5)" and "y = x * 2" above,
// y will not be equal to 10.
}
要点は、別のスレッドがチェックとアクトの間に x を変更したかどうかに応じて、y が 10 になるか、またはその他の値になる可能性があるということです。あなたには知る本当の方法がありません。
競合状態の発生を防ぐために、通常、共有データをロックして、一度に 1 つのスレッドのみがデータにアクセスできるようにします。これは次のような意味になります。
// Obtain lock for x
if (x == 5)
{
y = x * 2; // Now, nothing can change x until the lock is released.
// Therefore y = 10
}
// release lock for x
「競合状態」は、共有リソースにアクセスするマルチスレッド (または並列) コードが予期しない結果を引き起こす可能性がある場合に存在します。
次の例を見てください。
for ( int i = 0; i < 10000000; i++ )
{
x = x + 1;
}
このコードを一度に 5 つのスレッドで実行した場合、x の値が 50,000,000 になることはありません。実際には、実行ごとに異なります。
これは、各スレッドが x の値をインクリメントするために、次のことを行う必要があるためです: (簡略化して明らかに)
x の値を取得する この値に 1 を加算します この値を x に格納します
どのスレッドも、いつでもこのプロセスのどのステップにも入ることができ、共有リソースが関係している場合は、互いにステップを踏むことができます。x の状態は、x が読み取られてから書き戻されるまでの間、別のスレッドによって変更される可能性があります。
スレッドが x の値を取得したが、まだ格納していないとします。別のスレッドも x の同じ値を取得できます (スレッドがまだ変更されていないため)。その後、両方とも同じ値 (x+1) を xに格納します。
例:
スレッド 1: x を読み取り、値は 7 スレッド 1: x に 1 を追加すると、値は 8 になりました スレッド 2: x を読み取り、値は 7 スレッド 1: x に 8 を格納 スレッド 2: x に 1 を追加し、値は 8 になりました スレッド 2: x に 8 を格納
競合状態は、共有リソースにアクセスするコードの前にある種のロックメカニズムを採用することで回避できます。
for ( int i = 0; i < 10000000; i++ )
{
//lock x
x = x + 1;
//unlock x
}
ここでは毎回50,000,000と答えが出ます。
ロックの詳細については、ミューテックス、セマフォ、クリティカル セクション、共有リソースを検索してください。
レースコンディションとは?
あなたは午後5時に映画に行く予定です。午後 4 時にチケットの空き状況を問い合わせます。代表者は、それらが利用可能であると言います。リラックスして、ショーの 5 分前にチケット窓口に到着します。何が起こるかは想像できると思いますが、満室です。ここでの問題は、チェックとアクションの間の期間にありました。あなたは 4 で問い合わせ、5 で行動しました。その間に、他の誰かがチケットを手に入れました。これは競合状態です。具体的には、競合状態の「チェックしてから行動する」シナリオです。
それらをどのように検出しますか?
宗教的なコードのレビュー、マルチスレッドの単体テスト。近道はありません。これに関する Eclipse プラグインはほとんど登場していませんが、まだ安定しているものはありません。
それらをどのように処理し、防止しますか?
最善の方法は、副作用のないステートレスな関数を作成し、可能な限り不変を使用することです。しかし、それが常に可能であるとは限りません。そのため、java.util.concurrent.atomic、並行データ構造、適切な同期、およびアクター ベースの並行性を使用すると役立ちます。
並行性に最適なリソースは JCIP です。上記の説明の詳細については、こちらを参照してください。
競合状態とデータ競合の間には重要な技術的な違いがあります。ほとんどの回答は、これらの用語が同等であると仮定しているようですが、そうではありません。
データ競合は、2 つの命令が同じメモリ位置にアクセスし、これらのアクセスの少なくとも 1 つが書き込みであり、これらのアクセス間で順序付け前に発生しない場合に発生します。何が事前発生順序を構成するかについては多くの議論がありますが、一般に、同じロック変数の ulock-lock ペアと同じ条件変数の wait-signal ペアは、事前発生順序を引き起こします。
競合状態はセマンティック エラーです。これは、プログラムの誤った動作につながるイベントのタイミングまたは順序で発生する欠陥です。
多くの競合状態はデータ競合によって引き起こされる可能性があります (そして実際にはそうです) が、これは必須ではありません。実際のところ、データ競合と競合状態は、互いに必要条件でも十分条件でもありません。このブログ投稿でも、簡単な銀行取引の例を使用して、違いを非常によく説明しています。違いを説明する別の簡単な例を次に示します。
用語を明確にしたので、元の質問に答えてみましょう。
競合状態はセマンティック バグであるため、それらを検出する一般的な方法はありません。これは、一般的なケースで正しいプログラム動作と正しくないプログラム動作を区別できる自動化されたオラクルを持つ方法がないためです。レースの検出は決定不可能な問題です。
一方、データ競合は、必ずしも正確性とは関係のない正確な定義を持っているため、それらを検出することができます。データ競合検出器にはさまざまな種類があります (静的/動的データ競合検出、ロックセット ベースのデータ競合検出、前発生ベースのデータ競合検出、ハイブリッド データ競合検出)。最先端の動的データ競合検出器はThreadSanitizerで、実際には非常にうまく機能します。
一般に、データ競合を処理するには、共有データへのアクセス間 (開発中、または上記のツールを使用して検出された後) に事前発生エッジを誘導するためのプログラミング規則が必要です。これは、ロック、条件変数、セマフォなどを介して行うことができます。ただし、(共有メモリの代わりに) メッセージ パッシングなど、構造によるデータ競合を回避するさまざまなプログラミング パラダイムを使用することもできます。
一種の標準的な定義は、「2 つのスレッドが同時にメモリ内の同じ場所にアクセスし、アクセスの少なくとも 1 つが書き込みである場合」です。この状況では、「リーダー」スレッドは、どちらのスレッドが「レースに勝つ」かに応じて、古い値または新しい値を取得する場合があります。これは常にバグであるとは限りません。実際、非常に毛深い低レベルのアルゴリズムの中には、意図的にこれを行うものもありますが、一般的には回避する必要があります。@Steve Guryは、それが問題になる可能性がある場合の良い例です。
競合状態は一種のバグであり、特定の一時的な条件でのみ発生します。
例: A と B の 2 つのスレッドがあるとします。
スレッド A:
if( object.a != 0 )
object.avg = total / object.a
スレッド B:
object.a = 0
object.a が null でないことを確認した直後にスレッド A が横取りされた場合、B は実行しa = 0
、スレッド A がプロセッサを取得すると、「ゼロ除算」を実行します。
このバグは、if ステートメントの直後にスレッド A がプリエンプトされた場合にのみ発生します。非常にまれですが、発生する可能性があります。
競合状態とは、2 つの同時スレッドまたはプロセスがリソースを求めて競合し、結果の最終状態がリソースを最初に取得したユーザーに依存する、並行プログラミングの状況です。
競合状態は、マルチスレッド アプリケーションまたはマルチプロセス システムで発生します。最も基本的な競合状態とは、同じスレッドまたはプロセスにない 2 つのことが特定の順序で発生すると仮定し、それらが確実に発生するようにするための措置を講じないことです。これは、2 つのスレッドが両方ともアクセスできるクラスのメンバー変数を設定およびチェックすることによってメッセージを渡している場合によく発生します。あるスレッドが別のスレッドにタスクを完了する時間を与えるために sleep を呼び出すと、ほとんどの場合、競合状態が発生します (スリープが何らかのチェック メカニズムを使用してループしている場合を除きます)。
競合状態を防止するためのツールは言語と OS に依存しますが、一般的なツールとしてミューテックス、クリティカル セクション、シグナルがあります。ミューテックスは、自分だけが何かをしていることを確認したい場合に適しています。シグナルは、他の誰かが何かを完了したことを確認したい場合に適しています。共有リソースを最小限に抑えることも、予期しない動作を防ぐのに役立ちます
競合状態の検出は難しい場合がありますが、いくつかの兆候があります。スリープに大きく依存するコードは競合状態になりやすいため、まず影響を受けるコードでスリープの呼び出しを確認してください。特に長いスリープを追加すると、デバッグに使用して、イベントの特定の順序を強制することもできます。これは、動作を再現したり、物事のタイミングを変更することでそれを消すことができるかどうかを確認したり、導入されたソリューションをテストしたりするのに役立ちます。デバッグ後にスリープを削除する必要があります。
ただし、一部のマシンで断続的にしか発生しない問題がある場合は、競合状態があるという特徴的な兆候があります。一般的なバグは、クラッシュとデッドロックです。ロギングを使用すると、影響を受ける領域を見つけて、そこから作業を戻すことができるはずです。
Microsoft は実際に、この競合状態とデッドロックの問題について非常に詳細な記事を公開しています。それからの最も要約された要約は、タイトルの段落です。
2 つのスレッドが共有変数に同時にアクセスすると、競合状態が発生します。最初のスレッドが変数を読み取り、2 番目のスレッドが変数から同じ値を読み取ります。次に、最初のスレッドと 2 番目のスレッドが値に対して操作を実行し、どちらのスレッドが共有変数に最後に値を書き込むことができるかを競います。スレッドは前のスレッドが書き込んだ値を上書きしているため、最後に値を書き込んだスレッドの値は保持されます。
競合状態とは何ですか?
プロセスが他のイベントの順序またはタイミングに大きく依存している状況。
たとえば、プロセッサ A とプロセッサ Bは両方とも、実行に同じリソースを必要とします。
それらをどのように検出しますか?
競合状態を自動的に検出するツールがあります。
それらをどのように扱いますか?
競合状態はMutexまたはSemaphoresで処理できます。それらはロックとして機能し、競合状態を防ぐために、特定の要件に基づいてプロセスがリソースを取得できるようにします。
それらの発生をどのように防ぎますか?
Critical Section Avoidanceなど、競合状態を防ぐさまざまな方法があります。
競合状態は、デバイスまたはシステムが同時に 2 つ以上の操作を実行しようとしたときに発生する望ましくない状況ですが、デバイスまたはシステムの性質上、適切な順序で操作を実行する必要があります。正しく行われました。
コンピュータのメモリまたはストレージでは、大量のデータを読み書きするコマンドがほぼ同時に受信され、マシンが古いデータの一部またはすべてを上書きしようとした場合に競合状態が発生する可能性があります。読んだ。その結果として、コンピュータのクラッシュ、「不正な操作」、プログラムの通知とシャットダウン、古いデータの読み取りエラー、または新しいデータの書き込みエラーの 1 つまたは複数が発生する可能性があります。
これは、初心者がJavaのスレッドと競合状態を簡単に理解するのに役立つ、古典的な銀行口座残高の例です。
public class BankAccount {
/**
* @param args
*/
int accountNumber;
double accountBalance;
public synchronized boolean Deposit(double amount){
double newAccountBalance=0;
if(amount<=0){
return false;
}
else {
newAccountBalance = accountBalance+amount;
accountBalance=newAccountBalance;
return true;
}
}
public synchronized boolean Withdraw(double amount){
double newAccountBalance=0;
if(amount>accountBalance){
return false;
}
else{
newAccountBalance = accountBalance-amount;
accountBalance=newAccountBalance;
return true;
}
}
public static void main(String[] args) {
// TODO Auto-generated method stub
BankAccount b = new BankAccount();
b.accountBalance=2000;
System.out.println(b.Withdraw(3000));
}
カウントがインクリメントされるとすぐにカウントを表示する必要がある操作を考えてみましょう。つまり、CounterThreadが値をインクリメントするとすぐに、 DisplayThreadは最近更新された値を表示する必要があります。
int i = 0;
出力
CounterThread -> i = 1
DisplayThread -> i = 1
CounterThread -> i = 2
CounterThread -> i = 3
CounterThread -> i = 4
DisplayThread -> i = 4
ここで、 CounterThreadは頻繁にロックを取得し、 DisplayThreadが表示する前に値を更新します。ここに競合状態が存在します。競合状態は、同期を使用して解決できます