マルチスレッドアプリケーションを作成するときに発生する最も一般的な問題の1つは、競合状態です。
コミュニティへの私の質問は次のとおりです。
競合状態とは何ですか?
それらをどのように検出しますか?
それらをどのように扱いますか?
最後に、それらの発生をどのように防止しますか?
マルチスレッドアプリケーションを作成するときに発生する最も一般的な問題の1つは、競合状態です。
コミュニティへの私の質問は次のとおりです。
競合状態とは何ですか?
それらをどのように検出しますか?
それらをどのように扱いますか?
最後に、それらの発生をどのように防止しますか?
回答:
2つ以上のスレッドが共有データにアクセスでき、同時にそれらを変更しようとすると、競合状態が発生します。スレッドスケジューリングアルゴリズムはいつでもスレッド間でスワップできるため、スレッドが共有データへのアクセスを試みる順序はわかりません。したがって、データの変更の結果はスレッドスケジューリングアルゴリズムに依存します。つまり、両方のスレッドがデータにアクセス/変更するために「競合」しています。
1つのスレッドが「チェックしてからアクション」を実行すると(たとえば、値がXの場合は「チェック」、次に「アクション」を実行して、値がXであることに依存する何かを実行すると)問題が発生することがよくあります。 「チェック」と「行為」の間。例えば:
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:8をxに格納 スレッド2:xに1を追加、値は8 スレッド2:8をxに格納
共有リソースにアクセスするコードの前に何らかのロックメカニズムを採用することで、競合状態を回避できます。
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ペアと同じ条件変数の待機信号ペアは、発生前の順序を引き起こします。
競合状態はセマンティックエラーです。これは、イベントのタイミングまたは順序付けで発生する欠陥であり、プログラムの誤動作につながります。
多くの競合状態は、データの競合が原因で発生する可能性があります(実際には原因です)が、これは必須ではありません。実際のところ、データの競合と競合状態は、相互に必要でも十分条件でもありません。このブログ投稿では、簡単な銀行取引の例を使用して、違いを非常によく説明しています。これが違いを説明するもう1つの簡単な例です。
用語を書き留めたので、元の質問に答えてみましょう。
競合状態はセマンティックバグであるため、それらを検出する一般的な方法はありません。これは、一般的なケースでは、プログラムの正しい動作と正しくない動作を区別できる自動化されたオラクルを持つ方法がないためです。人種検出は、決定不可能な問題です。
一方、データの競合には正確性とは必ずしも関係のない正確な定義があるため、データの競合を検出できます。データ競合検出器には多くの種類があります(静的/動的データ競合検出、ロックセットベースのデータ競合検出、発生前のデータ競合検出、ハイブリッドデータ競合検出)。最先端の動的データレース検出器はThreadSanitizerであり、実際には非常によく機能します。
一般に、データ競合の処理には、共有データへのアクセスの間に発生前のエッジを誘発するためのプログラミング規則が必要です(開発中、または上記のツールを使用して検出された後)。これは、ロック、条件変数、セマフォなどを介して実行できます。ただし、メッセージパッシング(共有メモリではなく)などの異なるプログラミングパラダイムを使用して、構造によるデータの競合を回避することもできます。
ある種の標準的な定義は、「2つのスレッドが同時にメモリ内の同じ場所にアクセスし、少なくとも1つのアクセスが書き込みである場合」です。この状況では、「リーダー」スレッドは、「レースに勝った」スレッドに応じて、古い値または新しい値を取得します。これは常にバグであるとは限りません。実際、いくつかの非常に複雑な低レベルのアルゴリズムが故意にこれを行っていますが、通常は回避する必要があります。@Steve Guryは、問題となる可能性のある好例を示しています。
競合状態は一種のバグであり、特定の一時的な状態でのみ発生します。
例:2つのスレッドAとBがあるとします。
スレッド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つのプロセス」が互いに競合して「一部の共有状態に影響を与える」ことを意味します。スレッド/プロセスの起動順序、スレッド/プロセスのスケジューリングなど
競合状態とは、2つの並行スレッドまたはプロセスがリソースをめぐって競合し、最終的な状態が誰が最初にリソースを取得するかによって決まる並行プログラミングの状況です。
競合状態は、マルチスレッドアプリケーションまたはマルチプロセスシステムで発生します。競合状態は、最も基本的には、同じスレッドまたはプロセスにない2つのことが特定の順序で発生することを想定して、それらを確実に実行するための手順を実行しないものです。これは一般に、2つのスレッドがアクセスできるクラスのメンバー変数を設定およびチェックすることによってメッセージを渡すときに発生します。1つのスレッドがスリープを呼び出して別のスレッドにタスクを完了するための時間を与えるとき、ほとんどの場合競合状態があります(そのスリープがループしている場合を除き、いくつかのチェックメカニズムがあります)。
競合状態を防止するためのツールは言語とOSに依存しますが、いくつかの共通のものはミューテックス、クリティカルセクション、およびシグナルです。mutexは、自分だけが何かをしていることを確認したい場合に適しています。シグナルは、他の誰かが何かを終えたことを確認したい場合に適しています。共有リソースを最小限に抑えることで、予期しない動作を防ぐこともできます
競合状態の検出は難しい場合がありますが、いくつかの兆候があります。スリープに大きく依存しているコードは競合状態になりやすいため、まず影響を受けるコードでスリープへの呼び出しを確認します。特に長いスリープを追加することは、デバッグに使用して、特定の順序のイベントを強制的に試行することもできます。これは、振る舞いを再現したり、タイミングを変更することでそれを非表示にできるかどうかを確認したり、ソリューションをテストしたりするのに役立ちます。スリープはデバッグ後に削除する必要があります。
ただし、競合状態にあるというサインは、一部のマシンで断続的にのみ発生する問題がある場合に発生します。一般的なバグは、クラッシュとデッドロックです。ロギングを使用すると、影響を受ける領域を見つけてそこから作業を再開できるはずです。
マイクロソフトは実際、この競合状態とデッドロックの問題について非常に詳細な記事を公開しています。それから最も要約された要約は、タイトルの段落になります。
2つのスレッドが共有変数に同時にアクセスすると、競合状態が発生します。最初のスレッドは変数を読み取り、2番目のスレッドは変数から同じ値を読み取ります。次に、最初のスレッドと2番目のスレッドは値に対して操作を実行し、どのスレッドが値を最後に共有変数に書き込むことができるかを確認するために競合します。スレッドは、前のスレッドが書き込んだ値を上書きしているため、その値を最後に書き込むスレッドの値は保持されます。
競合状態とは何ですか?
プロセスが他のイベントのシーケンスまたはタイミングに大きく依存している状況。
たとえば、プロセッサAとプロセッサBの実行には、どちらも同じリソースが必要です。
それらをどのように検出しますか?
競合状態を自動的に検出するツールがあります:
それらをどのように扱いますか?
競合状態は、MutexまたはSemaphoresで処理できます。それらはロックとして機能し、プロセスが特定の要件に基づいてリソースを取得し、競合状態を防止できるようにします。
それらが発生するのをどのように防ぐのですか?
クリティカルセクションの回避など、競合状態を回避するにはさまざまな方法があります。
競合状態は、デバイスまたはシステムが同時に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));
}
「アトミック」クラスを使用すると、競合状態を回避できます。その理由は、スレッドが操作の取得と設定を分離しないためです。以下に例を示します。
AtomicInteger ai = new AtomicInteger(2);
ai.getAndAdd(5);
その結果、リンク "ai"に7があります。2つのアクションを実行しましたが、両方の操作で同じスレッドが確認され、他のスレッドがこれに干渉することはありません。つまり、競合状態は発生しません。
競合状態をよりよく理解するには、次の基本的な例を試してください。
public class ThreadRaceCondition {
/**
* @param args
* @throws InterruptedException
*/
public static void main(String[] args) throws InterruptedException {
Account myAccount = new Account(22222222);
// Expected deposit: 250
for (int i = 0; i < 50; i++) {
Transaction t = new Transaction(myAccount,
Transaction.TransactionType.DEPOSIT, 5.00);
t.start();
}
// Expected withdrawal: 50
for (int i = 0; i < 50; i++) {
Transaction t = new Transaction(myAccount,
Transaction.TransactionType.WITHDRAW, 1.00);
t.start();
}
// Temporary sleep to ensure all threads are completed. Don't use in
// realworld :-)
Thread.sleep(1000);
// Expected account balance is 200
System.out.println("Final Account Balance: "
+ myAccount.getAccountBalance());
}
}
class Transaction extends Thread {
public static enum TransactionType {
DEPOSIT(1), WITHDRAW(2);
private int value;
private TransactionType(int value) {
this.value = value;
}
public int getValue() {
return value;
}
};
private TransactionType transactionType;
private Account account;
private double amount;
/*
* If transactionType == 1, deposit else if transactionType == 2 withdraw
*/
public Transaction(Account account, TransactionType transactionType,
double amount) {
this.transactionType = transactionType;
this.account = account;
this.amount = amount;
}
public void run() {
switch (this.transactionType) {
case DEPOSIT:
deposit();
printBalance();
break;
case WITHDRAW:
withdraw();
printBalance();
break;
default:
System.out.println("NOT A VALID TRANSACTION");
}
;
}
public void deposit() {
this.account.deposit(this.amount);
}
public void withdraw() {
this.account.withdraw(amount);
}
public void printBalance() {
System.out.println(Thread.currentThread().getName()
+ " : TransactionType: " + this.transactionType + ", Amount: "
+ this.amount);
System.out.println("Account Balance: "
+ this.account.getAccountBalance());
}
}
class Account {
private int accountNumber;
private double accountBalance;
public int getAccountNumber() {
return accountNumber;
}
public double getAccountBalance() {
return accountBalance;
}
public Account(int accountNumber) {
this.accountNumber = accountNumber;
}
// If this method is not synchronized, you will see race condition on
// Remove syncronized keyword to see race condition
public synchronized boolean deposit(double amount) {
if (amount < 0) {
return false;
} else {
accountBalance = accountBalance + amount;
return true;
}
}
// If this method is not synchronized, you will see race condition on
// Remove syncronized keyword to see race condition
public synchronized boolean withdraw(double amount) {
if (amount > accountBalance) {
return false;
} else {
accountBalance = accountBalance - amount;
return true;
}
}
}
常に競合状態を破棄する必要はありません。複数のスレッドが読み書きできるフラグがあり、このフラグが1つのスレッドによって「完了」に設定されているため、フラグが「完了」に設定されているときに他のスレッドが処理を停止する場合、その「競合」は望ましくありません。条件」を削除します。実際、これは良性の競合状態と呼ばれます。
ただし、競合状態を検出するツールを使用すると、有害な競合状態として検出されます。
競合状態の詳細については、http://msdn.microsoft.com/en-us/magazine/cc546569.aspxを参照してください。
カウントがインクリメントされるとすぐにカウントを表示する必要がある操作を検討してください。つまり、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が表示する前に値を更新します。ここに競合状態があります。競合状態は同期を使用して解決できます