競合状態とは何ですか?


982

マルチスレッドアプリケーションを作成するときに発生する最も一般的な問題の1つは、競合状態です。

コミュニティへの私の質問は次のとおりです。

競合状態とは何ですか?
それらをどのように検出しますか?
それらをどのように扱いますか?
最後に、それらの発生をどのように防止しますか?


3
Secure Programming for Linux HOWTOには、それらが何であり、どのように回避するかを説明した素晴らしい章があります。
Craig H

4
言語を指定しないと、この質問のほとんどの部分は適切に回答できません。異なる言語では、定義、結果、それらを防ぐためのツールが異なる可能性があるためです。
MikeMB 2015

@MikeMB。Race Catcher(このスレッドstackoverflow.com/a/29361427/1363844を参照)のようにバイトコードの実行を分析する場合を除いて、バイトコードにコンパイルされる約62言語すべてに対応できることに同意しました(en.wikipedia.orgを参照)/ wiki / List_of_JVM_languages
Ben

回答:


1238

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

121
ロックに遭遇したとき、他のスレッドは何をしますか?待ってますか?エラー?
Brian Ortiz、

174
はい、他のスレッドは、続行する前にロックが解放されるまで待機する必要があります。このため、ロックが終了すると、保持スレッドによってロックが解放されることが非常に重要になります。それが解放されない場合、他のスレッドは無期限に待機します。
Lehane、

2
@Ianマルチスレッドシステムでは、リソースを共有する必要がある場合が常にあります。代替案を提供せずに1つのアプローチが悪いと言っても、生産的ではありません。私は常に改善する方法を探しています。代替案があれば、喜んで調査し、賛否両論を比較検討します。
デスパタール

2
@Despertar ...また、必ずしもマルチスレッドシステムでリソースを共有する必要があるとは限りません。たとえば、各要素が処理を必要とする配列があるとします。配列をパーティション化し、各パーティションにスレッドを設定すると、スレッドは互いに完全に独立して作業を実行できます。
Ian Warburton、

12
競合が発生するには、単一のスレッドが共有データを変更しようとするだけで十分ですが、残りのスレッドは共有データを読み取りまたは変更できます。
SomeWittyUsername

213

「競合状態」は、共有リソースにアクセスするマルチスレッド(または並列)コードが予期しない結果を引き起こすような方法で実行できる場合に存在します。

この例を見てみましょう:

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と答えが出ます。

ロックの詳細については、ミューテックス、セマフォ、クリティカルセクション、共有リソースを検索してください。


そのようなことがどれほどうまくいかないかをテストするプログラムの例については、jakob.engbloms.se / archives / 65を参照してください...実際に実行しているマシンのメモリモデルによって異なります。
jakobengblom2 2008年

1
1000万で停止する必要がある場合、どうすれば5000万に到達できますか?

9
@nocomprende:スニペットのすぐ下に記載されているように、一度に5つのスレッドが同じコードを実行する...
Jon Skeet

4
@JonSkeetあなたは正しい、私はiとxを混同した。ありがとうございました。

シングルトンパターンの実装でのダブルチェックロックは、競合状態を防止する例です。
Bharat Dodeja

150

競合状態とは何ですか?

午後5時に映画を見に行く予定です。午後4時にチケットの在庫について問い合わせます。担当者はそれらが利用可能であると言います。ショーの5分前にリラックスしてチケットウィンドウに到着します。私はあなたが何が起こるかを推測できると確信しています:それは完全な家です。ここでの問題は、チェックとアクションの間の期間にありました。あなたは4時に問い合わせ、5時に行動しました。その間、他の誰かがチケットをつかみました。それが競合状態です。具体的には、競合状態の「確認後処理」シナリオです。

それらをどのように検出しますか?

宗教的なコードレビュー、マルチスレッドのユニットテスト。近道はありません。これにはいくつかのEclipseプラグインが登場していますが、まだ安定していません。

それらをどのように扱い、防止しますか?

最善の方法は、副作用のない、ステートレスな関数を作成し、できるだけ不変のものを使用することです。しかし、それが常に可能であるとは限りません。したがって、java.util.concurrent.atomic、並行データ構造、適切な同期、アクターベースの同時実行を使用すると役立ちます。

同時実行に最適なリソースはJCIPです。あなたはまた、いくつかのより多く得ることができ、ここで、上記の説明の詳細を


コードレビューと単体テストは、耳の間の流れをモデル化し、共有メモリの使用量を少なくすることの二次的なものです。
Acumenus 2013年

2
レース状態の実例に感謝しました
トムO.

11
答えが気に入りました。解決策は次のとおりです:ミューテックス(相互例外、c ++)で4-5の間のチケットをロックします。実際には、チケット予約と呼ばれます:)
Volt

1
Javaのみのビットを削除した場合、それは適切な答えになります(問題はJavaに関するものではなく、一般に競合状態です)
Corey Goldberg

いいえ、これは競合状態ではありません。「ビジネス」の観点からは、あまりにも長く待っていました。明らかに、バックオーダーは解決策ではありません。スキャルパーを試してみてください。それ以外の場合は、チケットを保険として購入してください
csherriff

65

競合状態とデータ競合の間には、重要な技術的な違いがあります。ほとんどの回答では、これらの用語は同等であると想定していますが、そうではありません。

2つの命令が同じメモリ位置にアクセスすると、データの競合が発生します。これらのアクセスの少なくとも1つは書き込みであり、これらのアクセス間で順序付けする前に発生しません。ここで、順序付けの前に発生することについては、多くの議論の対象となりますが、一般に、同じロック変数のulock-lockペアと同じ条件変数の待機信号ペアは、発生前の順序を引き起こします。

競合状態はセマンティックエラーです。これは、イベントのタイミングまたは順序付けで発生する欠陥であり、プログラムの誤動作につながります

多くの競合状態は、データの競合が原因で発生する可能性があります(実際には原因です)が、これは必須ではありません。実際のところ、データの競合と競合状態は、相互に必要でも十分条件でもありません。このブログ投稿では、簡単な銀行取引の例を使用して、違いを非常によく説明しています。これが違いを説明するもう1つの簡単なです。

用語を書き留めたので、元の質問に答えてみましょう。

競合状態はセマンティックバグであるため、それらを検出する一般的な方法はありません。これは、一般的なケースでは、プログラムの正しい動作と正しくない動作を区別できる自動化されたオラクルを持つ方法がないためです。人種検出は、決定不可能な問題です。

一方、データの競合には正確性とは必ずしも関係のない正確な定義があるため、データの競合を検出できます。データ競合検出器には多くの種類があります(静的/動的データ競合検出、ロックセットベースのデータ競合検出、発生前のデータ競合検出、ハイブリッドデータ競合検出)。最先端の動的データレース検出器はThreadSanitizerであり、実際には非常によく機能します。

一般に、データ競合の処理には、共有データへのアクセスの間に発生前のエッジを誘発するためのプログラミング規則が必要です(開発中、または上記のツールを使用して検出された後)。これは、ロック、条件変数、セマフォなどを介して実行できます。ただし、メッセージパッシング(共有メモリではなく)などの異なるプログラミングパラダイムを使用して、構造によるデータの競合を回避することもできます。


違いは、競合状態を理解するために重要です。ありがとう!
ProgramCpp 2018

37

ある種の標準的な定義は、「2つのスレッドが同時にメモリ内の同じ場所にアクセスし、少なくとも1つのアクセスが書き込みである場合」です。この状況では、「リーダー」スレッドは、「レースに勝った」スレッドに応じて、古い値または新しい値を取得します。これは常にバグであるとは限りません。実際、いくつかの非常に複雑な低レベルのアルゴリズムが故意にこれを行っていますが、通常は回避する必要があります。@Steve Guryは、問題となる可能性のある好例を示しています。


3
競合状態がどのように役立つか例を挙げていただけますか?グーグルは助けにはならなかった。
Alex V.

3
@Alex V.現時点では、何について話しているのかわかりません。これはロックフリープログラミングへの参照だったと思いますが、それ自体が競合状態に依存すると言うのは本当に正確ではありません。
Chris Conway、

33

競合状態は一種のバグであり、特定の一時的な状態でのみ発生します。

例: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がプリエンプトされたときにのみ発生します。これは非常にまれですが、発生する可能性があります。


21

競合状態はソフトウェアだけでなく、ハードウェアにも関連しています。実際、この用語は最初はハードウェア業界によって作り出されました。

ウィキペディアによると:

この用語は、最初に出力影響与えるため2つの信号が互いに競合するという考えに由来しています

論理回路の競合状態:

ここに画像の説明を入力してください

ソフトウェア業界はこの用語を変更せずに採用したため、理解するのが少し難しくなっています。

それをソフトウェアの世界にマッピングするには、いくつかの置換を行う必要があります。

  • 「2つのシグナル」=>「2つのスレッド」/「2つのプロセス」
  • "出力に影響を与える" => "いくつかの共有状態に影響を与える"

したがって、ソフトウェア業界の競合状態とは、「2つのスレッド」/「2つのプロセス」が互いに競合して「一部の共有状態に影響を与える」ことを意味します。スレッド/プロセスの起動順序、スレッド/プロセスのスケジューリングなど


20

競合状態とは、2つの並行スレッドまたはプロセスがリソースをめぐって競合し、最終的な状態が誰が最初にリソースを取得するかによって決まる並行プログラミングの状況です。


ちょうど華麗な説明
gokareless


1
@RomanAlexandrovichプログラムの最終状態。変数の値などを参照する状態。レハネの優れた答えを参照してください。彼の例の「状態」は、「x」と「y」の最終値を指します。
AMTerp

19

競合状態は、マルチスレッドアプリケーションまたはマルチプロセスシステムで発生します。競合状態は、最も基本的には、同じスレッドまたはプロセスにない2つのことが特定の順序で発生することを想定して、それらを確実に実行するための手順を実行しないものです。これは一般に、2つのスレッドがアクセスできるクラスのメンバー変数を設定およびチェックすることによってメッセージを渡すときに発生します。1つのスレッドがスリープを呼び出して別のスレッドにタスクを完了するための時間を与えるとき、ほとんどの場合競合状態があります(そのスリープがループしている場合を除き、いくつかのチェックメカニズムがあります)。

競合状態を防止するためのツールは言語とOSに依存しますが、いくつかの共通のものはミューテックス、クリティカルセクション、およびシグナルです。mutexは、自分だけが何かをしていることを確認したい場合に適しています。シグナルは、他の誰かが何かを終えたことを確認したい場合に適しています。共有リソースを最小限に抑えることで、予期しない動作を防ぐこともできます

競合状態の検出は難しい場合がありますが、いくつかの兆候があります。スリープに大きく依存しているコードは競合状態になりやすいため、まず影響を受けるコードでスリープへの呼び出しを確認します。特に長いスリープを追加することは、デバッグに使用して、特定の順序のイベントを強制的に試行することもできます。これは、振る舞いを再現したり、タイミングを変更することでそれを非表示にできるかどうかを確認したり、ソリューションをテストしたりするのに役立ちます。スリープはデバッグ後に削除する必要があります。

ただし、競合状態にあるというサインは、一部のマシンで断続的にのみ発生する問題がある場合に発生します。一般的なバグは、クラッシュとデッドロックです。ロギングを使用すると、影響を受ける領域を見つけてそこから作業を再開できるはずです。


10

マイクロソフトは実際、この競合状態とデッドロックの問題について非常に詳細な記事を公​​開しています。それから最も要約された要約は、タイトルの段落になります。

2つのスレッドが共有変数に同時にアクセスすると、競合状態が発生します。最初のスレッドは変数を読み取り、2番目のスレッドは変数から同じ値を読み取ります。次に、最初のスレッドと2番目のスレッドは値に対して操作を実行し、どのスレッドが値を最後に共有変数に書き込むことができるかを確認するために競合します。スレッドは、前のスレッドが書き込んだ値を上書きしているため、その値を最後に書き込むスレッドの値は保持されます。


5

競合状態とは何ですか?

プロセスが他のイベントのシーケンスまたはタイミングに大きく依存している状況。

たとえば、プロセッサAとプロセッサBの実行には、どちらも同じリソースが必要です。

それらをどのように検出しますか?

競合状態を自動的に検出するツールがあります:

それらをどのように扱いますか?

競合状態は、MutexまたはSemaphoresで処理できます。それらはロックとして機能し、プロセスが特定の要件に基づいてリソースを取得し、競合状態を防止できるようにします。

それらが発生するのをどのように防ぐのですか?

クリティカルセクションの回避など、競合状態を回避するにはさまざまな方法があります。

  1. 重要な領域内で同時に2つのプロセスはありません。(相互排除)
  2. 速度やCPUの数は想定されていません。
  3. 他のプロセスをブロックするクリティカル領域外で実行されているプロセスはありません。
  4. 重要な領域に入るのを永遠に待つ必要のあるプロセスはありません。(AはBリソースを待ち、BはCリソースを待ち、CはAリソースを待ちます)

2

競合状態は、デバイスまたはシステムが同時に2つ以上の操作を実行しようとしたときに発生する望ましくない状況ですが、デバイスまたはシステムの性質上、操作を適切な順序で実行する必要があります。正しく行われました。

コンピュータのメモリまたはストレージでは、大量のデータを読み書きするコマンドがほぼ同時に受信され、古いデータがまだ残っている間にマシンが古いデータの一部またはすべてを上書きしようとすると、競合状態が発生する可能性があります読んだ。結果は、コンピュータのクラッシュ、「不正な操作」、プログラムの通知とシャットダウン、古いデータの読み取りエラー、または新しいデータの書き込みエラーの1つ以上になる可能性があります。


2

これは、初心者が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));

}

1

「アトミック」クラスを使用すると、競合状態回避できます。その理由は、スレッドが操作の取得と設定を分離しないためです。以下に例を示します。

AtomicInteger ai = new AtomicInteger(2);
ai.getAndAdd(5);

その結果、リンク "ai"に7があります。2つのアクションを実行しましたが、両方の操作で同じスレッドが確認され、他のスレッドがこれに干渉することはありません。つまり、競合状態は発生しません。


0

競合状態をよりよく理解するには、次の基本的な例を試してください。

    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;
        }
    }
}

0

常に競合状態を破棄する必要はありません。複数のスレッドが読み書きできるフラグがあり、このフラグが1つのスレッドによって「完了」に設定されているため、フラグが「完了」に設定されているときに他のスレッドが処理を停止する場合、その「競合」は望ましくありません。条件」を削除します。実際、これは良性の競合状態と呼ばれます。

ただし、競合状態を検出するツールを使用すると、有害な競合状態として検出されます。

競合状態の詳細については、http://msdn.microsoft.com/en-us/magazine/cc546569.aspxを参照してください


あなたの答えはどの言語に基づいていますか?
MikeMB 2015

率直に言って、競合状態自体がある場合、厳密に制御された方法でコードを設計していないようです。これは、理論的なケースでは問題ではないかもしれませんが、ソフトウェアの設計と開発の方法に大きな問題があることの証拠です。遅かれ早かれ、痛みを伴う競合状態のバグに直面することを期待してください。
エンジニア

0

カウントがインクリメントされるとすぐにカウントを表示する必要がある操作を検討してください。つまり、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が表示する前に値を更新します。ここに競合状態があります。競合状態は同期を使用して解決できます


0

競合状態は、2つ以上のプロセスが同時に共有データにアクセスして変更できる場合に発生する望ましくない状況です。リソースへのアクセスの競合があったために発生しました。クリティカルセクションの問題により、競合状態が発生する可能性があります。プロセス間のクリティカル状態を解決するために、クリティカルセクションを実行するプロセスを一度に1つだけ取り出します。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.