「データの競合」と「競合状態」は、並行プログラミングのコンテキストでは実際には同じものです


回答:


138

いいえ、同じではありません。それらは互いのサブセットではありません。それらはまた、お互いに必要でも十分条件でもありません。

データ競合の定義はかなり明確であるため、その検出は自動化できます。異なるスレッドからの2つの命令が同一のメモリ位置にアクセスするときにデータの競合が発生し、これらのアクセスの少なくとも一方が書き込みであり、義務されない同期化が存在しない任意のこれらのアクセスのうちの特定の順序は。

競合状態は意味上のエラーです。誤ったプログラムの動作につながるイベントのタイミングまたは順序に発生する欠陥です。多くの競合状態はデータ競合によって引き起こされる可能性がありますが、これは必須ではありません。

xが共有変数である次の簡単な例を考えます。

Thread 1    Thread 2

 lock(l)     lock(l)
 x=1         x=2
 unlock(l)   unlock(l)

この例では、スレッド1と2からのxへの書き込みはロックによって保護されているため、実行時にロックが取得される順序によって強制される順序で常に発生します。つまり、書き込みの原子性を壊すことはできません。どの実行でも、2つの書き込みの間に常に前に発生が発生します。どの書き込みが他のアプリオリの前に行われるかはわかりません。

ロックはこれを提供できないため、書き込み間の固定順序はありません。スレッド2によるxへの書き込みの後にスレッド1でxへの書き込みが続く場合など、プログラムの正確さが損なわれている場合、技術的にはデータの競合はありませんが、競合状態があると言えます。

データ競合よりも競合状態を検出する方がはるかに便利です。ただし、これを達成することも非常に困難です。

逆の例を作成するのも簡単です。このブログ投稿では、簡単な銀行取引の例を使用して、違いを非常によく説明しています。


「データ競合(...)これらのアクセス間の特定の順序を強制する同期はありません。」私は少し混乱しています。あなたの例では、操作は両方の順序で発生する可能性があります(= 1の次に= 2、またはその逆)。なぜデータ競合ではないのですか?
josinalvo 2015

5
@josinalvo:データレースの技術的な定義の成果物です。重要な点は、2つのアクセスの間に、ロックの解放とロックの取得(可能な順序のいずれか1つ)があるということです。定義により、ロック解放とロック獲得は2つのアクセス間の順序を確立するため、データ競合は発生しません。
Baris Kasikci 2015

同期は、操作間の特定の順序を強制することは決してないため、これはそれを表現するための非常に幸運な方法ではありません。一方、JMMは、読み取り操作ごとに、データレースであっても、監視する明確な書き込み操作が存在する必要があることを指定しています。発生前と同期の順序を明示的に言及することを避けるのは難しいですが、JLSの定義でさえ、発生前のみを言及するのは間違っています。その定義により、2つの同時揮発性書き込みがデータ競合を構成します。
Marko Topolnik 2017

@BarisKasikciは、「注文を確定する」というのは、私に関する限り、本当の意味はありません。彼らはただのイタチの言葉です。文字通り、複数のスレッドからアクセスされますすべてのメモリ位置は、「データ競合」であるとみなすことができるように私は正直、「データのレースは、」リモートで便利な概念であるとは考えていない
Noldorin

リリース取得ペアは常に順序を確立します。一般的な説明は長くなりますが、簡単な例はシグナルとウェイトのペアです。@Noldorin「順序を確立する」は、同時発生理論(Lamportの「発生前の関係」に関する重要な論文を参照)および分散システムの主要な概念である「発生前の順序」を指します。データの競合は、それらの存在が多くの問題(たとえば、C ++メモリモデルによる未定義のセマンティクス、Javaでの非常に複雑なセマンティクスなど)を引き起こすという点で、有用な概念です。それらの検出と排除は、研究と実践における膨大な文献を構成します。
Baris Kasikci

20

ウィキペディアによると、「競合状態」という用語は、最初の電子論理ゲートの時代から使用されています。Javaのコンテキストでは、競合状態は、ファイル、ネットワーク接続、スレッドプールからのスレッドなど、あらゆるリソースに関係する可能性があります。

「データ競合」という用語は、JLSによって定義された特定の意味のために予約するのが最善です。

最も興味深いケースは、データの競合に非常に似ているが、次の単純な例のようにまだ競合状態ではない競合状態です。

class Race {
  static volatile int i;
  static int uniqueInt() { return i++; }
}

iは揮発性であるため、データの競合はありません。ただし、プログラムの正確さの観点からは、読み取りi、書き込みの2つの操作の非原子性が原因で競合状態が発生しますi+1。複数のスレッドがから同じ値を受け取ることがありますuniqueInt


1
data raceJLSで実際に何を意味するかを説明するあなたの答えに一行を入れることができますか?
オタク

@geek「JLS」という言葉は、JLSの関連セクションへのハイパーリンクです。
Marko Topolnik 2013

@MarkoTopolnik私はこの例に少し混乱しています。「私は不安定なため、データ競合はありません」と説明していただけませんか?Voltilityはそれが表示されていることを確認するだけですが、それでも1)同期されておらず、複数のスレッドが同時に読み取り/書き込みを行うことができます。2)非最終フィールドを共有している、それはデータ競合であり、競合状態ではありませんか?
aniliitb10

@ aniliitb10コンテキストから引き裂かれた中古の引用に依存する代わりに、私の回答でリンクしたJLSセクション17.4を確認する必要があります。揮発性変数へのアクセスは、§17.4.2で定義されている同期アクションです。
Marko Topolnik

@ aniliitb10 Votaltilesはアクセスを注文できるため、データ競合を引き起こしません。つまり、この方法またはその方法でそれらの順序を推論して、異なる結果を導くことができます。データレースでは、注文を推論する方法はありません。たとえば、各スレッドのi ++操作は、それぞれのローカルにキャッシュされた値iで発生する可能性があります。グローバルに(プログラマーの観点から)これらの操作を順序付ける方法はありません-特定の言語メモリモデルを配置している場合を除きます。
Xiao-Feng Li

3

いいえ、違います。どちらも1つのサブセットではなく、その逆も同様です。

競合状態という用語は、関連する用語のデータ競合と混同されることがよくあります。これは、同期を使用して共有の非最終フィールドへのすべてのアクセスを調整しない場合に発生します。スレッドが別のスレッドによって次に読み取られる可能性がある変数を書き込む場合、または両方のスレッドが同期を使用しない場合、別のスレッドによって最後に書き込まれた可能性がある変数を読み取る場合は常に、データ競合のリスクがあります。データ競合のあるコードには、Javaメモリモデルでの有用な定義されたセマンティクスがありません。すべての競合状態がデータ競合であるとは限りません。また、すべてのデータ競合が競合状態であるとは限りませんが、どちらも並行プログラムが予期しない方法で失敗する原因となる可能性があります。

Joshua Bloch&Co.による優れた本-Java Concurrency in Practiceからの抜粋


質問には言語に依存しないタグがあることに注意してください。
martinkunev

1

TL; DR:データの競合と競合状態の違いは、問題の定式化の性質、および未定義の動作と明確に定義されているが不確定な動作との境界をどこに描くかによって異なります。現在の違いは従来のものであり、プロセッサーアーキテクトとプログラミング言語の間のインターフェースを最もよく反映しています。

1.セマンティクス

データ競合とは、特に、同じメモリ位置への非同期の競合する「メモリアクセス」(またはアクション、または操作)を指します。メモリのアクセスに競合がなく、操作の順序付けによって引き起こされる不確定な動作がまだある場合、それは競合状態です。

ここでの「メモリアクセス」には特定の意味があります。これらは、追加のセマンティクスが適用されていない、「純粋な」メモリのロードまたはストアアクションを参照します。たとえば、あるスレッドのメモリストアは、データがメモリに書き込まれるのにかかる時間を(必ずしも)知らず、最終的に別のスレッドに伝播します。別の例では、ある場所へのメモリストアが同じスレッドによる別の場所への別のストアの前に、(必ずしも)メモリに書き込まれた最初のデータが2番目のデータよりも前にあることを保証しません。その結果、これらの純粋なメモリアクセスの順序は(必ずしも)「推論」することができず、特に明確に定義されていない限り、何かが発生する可能性があります。

「メモリアクセス」が同期による順序付けに関して明確に定義されている場合、追加のセマンティクスにより、メモリアクセスのタイミングが不確定であっても、同期を通じてその順序を「推論」することができます。メモリアクセス間の順序付けには理由がありますが、必ずしも確定しているわけではないため、競合状態になっていることに注意してください。

2.なぜ違いがあるのですか?

しかし、注文が依然として競合状態で不確定である場合、なぜそれをデータ競合と区別するのが面倒なのでしょうか。その理由は、理論的ではなく実用的です。これは、プログラミング言語とプロセッサアーキテクチャのインターフェイスに違いがあるためです。

モダンアーキテクチャのメモリロード/ストア命令は、アウトオブオーダーパイプライン、スペキュレーション、マルチレベルのキャッシュ、CPU RAM相互接続、特にマルチコアなどの性質により、通常「純粋な」メモリアクセスとして実装されます。 。タイミングと順序が不確定になる要因はたくさんあります。すべてのメモリ命令の順序付けを強制するには、特にマルチコアをサポートするプロセッサデザインでは、大きなペナルティが発生します。そのため、順序付けのセマンティクスには、さまざまなバリア(またはフェンス)などの追加の指示が用意されています。

データ競合は、競合するメモリアクセスの順序付けを推論するのに役立つ追加のフェンスなしのプロセッサ命令実行の状況です。結果は不確定であるだけでなく、非常に奇妙なこともあります。たとえば、異なるスレッドによる同じワードの場所への2つの書き込みは、ワードの半分の書き込みごとに発生するか、ローカルにキャッシュされた値でのみ動作する可能性があります。-これらは、プログラマーの観点から、未定義の動作です。しかし、それらは(通常)プロセッサアーキテクトの観点から明確に定義されています。

プログラマーはコードの実行を推論する方法を持っている必要があります。データの競合は彼らが意味をなさないものなので、常に(通常は)回避すべきです。そのため、十分に低レベルの言語仕様では、データ競合を未定義の動作として定義し、競合状態の明確に定義されたメモリ動作とは異なります。

3.言語記憶モデル

プロセッサが異なれば、メモリアクセス動作も異なります(つまり、プロセッサメモリモデル)。プログラマが現代のすべてのプロセッサのメモリモデルを研究し、それらから利益を得ることができるプログラムを開発するのは厄介です。その言語のプログラムが常にメモリモデルの定義どおりに期待どおりに動作するように、言語でメモリモデルを定義できることが望ましいです。これが、JavaとC ++でメモリモデルが定義されている理由です。言語メモリモデルが異なるプロセッサアーキテクチャに確実に適用されるようにするのは、コンパイラ/ランタイム開発者の負担です。

とは言っても、言語がプロセッサの低レベルの動作を公開したくない場合(そして、現代のアーキテクチャの特定のパフォーマンス上の利点を犠牲にしても構わない場合)、「純粋な」の詳細を完全に隠すメモリモデルを定義することを選択できます。メモリアクセス、ただしすべてのメモリ操作に順序付けセマンティクスを適用します。次に、コンパイラ/ランタイムの開発者は、すべてのプロセッサアーキテクチャですべてのメモリ変数を揮発性として扱うことを選択できます。これらの言語(スレッド間で共有メモリをサポートする)の場合、データの競合はありませんが、完全な順次一貫性のある言語であっても、競合状態になる可能性があります。

一方、プロセッサのメモリモデルは、より厳格(または緩やか、またはより高いレベル)にすることができます。たとえば、初期のプロセッサと同じように逐次一貫性を実装します。次に、すべてのメモリ操作が順序付けられ、プロセッサで実行されている言語のデータ競合は発生しません。

4.まとめ

元の質問に戻りますが、私見では、データ競合を競合状態の特殊なケースとして定義しても問題ありません。あるレベルの競合状態は、より高いレベルのデータ競合になる場合があります。問題の定式化の性質、および未定義の動作と明確に定義されているが不確定な動作との境界をどこに描くかによって異なります。現在の規則だけが言語プロセッサインターフェースで境界を定義していますが、必ずしもそうである必要はありません。しかし、現在の規則はおそらく、プロセッサアーキテクトとプログラミング言語の間の最先端のインターフェース(および知恵)を最もよく反映しています。

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