これらの用語は、並行プログラミングのコンテキストで使用されていることがよくあります。それらは同じものですか、それとも異なりますか?
回答:
いいえ、同じではありません。それらは互いのサブセットではありません。それらはまた、お互いに必要でも十分条件でもありません。
データ競合の定義はかなり明確であるため、その検出は自動化できます。異なるスレッドからの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への書き込みが続く場合など、プログラムの正確さが損なわれている場合、技術的にはデータの競合はありませんが、競合状態があると言えます。
データ競合よりも競合状態を検出する方がはるかに便利です。ただし、これを達成することも非常に困難です。
逆の例を作成するのも簡単です。このブログ投稿では、簡単な銀行取引の例を使用して、違いを非常によく説明しています。
ウィキペディアによると、「競合状態」という用語は、最初の電子論理ゲートの時代から使用されています。Javaのコンテキストでは、競合状態は、ファイル、ネットワーク接続、スレッドプールからのスレッドなど、あらゆるリソースに関係する可能性があります。
「データ競合」という用語は、JLSによって定義された特定の意味のために予約するのが最善です。
最も興味深いケースは、データの競合に非常に似ているが、次の単純な例のようにまだ競合状態ではない競合状態です。
class Race {
static volatile int i;
static int uniqueInt() { return i++; }
}
i
は揮発性であるため、データの競合はありません。ただし、プログラムの正確さの観点からは、読み取りi
、書き込みの2つの操作の非原子性が原因で競合状態が発生しますi+1
。複数のスレッドがから同じ値を受け取ることがありますuniqueInt
。
data race
JLSで実際に何を意味するかを説明するあなたの答えに一行を入れることができますか?
いいえ、違います。どちらも1つのサブセットではなく、その逆も同様です。
競合状態という用語は、関連する用語のデータ競合と混同されることがよくあります。これは、同期を使用して共有の非最終フィールドへのすべてのアクセスを調整しない場合に発生します。スレッドが別のスレッドによって次に読み取られる可能性がある変数を書き込む場合、または両方のスレッドが同期を使用しない場合、別のスレッドによって最後に書き込まれた可能性がある変数を読み取る場合は常に、データ競合のリスクがあります。データ競合のあるコードには、Javaメモリモデルでの有用な定義されたセマンティクスがありません。すべての競合状態がデータ競合であるとは限りません。また、すべてのデータ競合が競合状態であるとは限りませんが、どちらも並行プログラムが予期しない方法で失敗する原因となる可能性があります。
Joshua Bloch&Co.による優れた本-Java Concurrency in Practiceからの抜粋
TL; DR:データの競合と競合状態の違いは、問題の定式化の性質、および未定義の動作と明確に定義されているが不確定な動作との境界をどこに描くかによって異なります。現在の違いは従来のものであり、プロセッサーアーキテクトとプログラミング言語の間のインターフェースを最もよく反映しています。
1.セマンティクス
データ競合とは、特に、同じメモリ位置への非同期の競合する「メモリアクセス」(またはアクション、または操作)を指します。メモリのアクセスに競合がなく、操作の順序付けによって引き起こされる不確定な動作がまだある場合、それは競合状態です。
ここでの「メモリアクセス」には特定の意味があります。これらは、追加のセマンティクスが適用されていない、「純粋な」メモリのロードまたはストアアクションを参照します。たとえば、あるスレッドのメモリストアは、データがメモリに書き込まれるのにかかる時間を(必ずしも)知らず、最終的に別のスレッドに伝播します。別の例では、ある場所へのメモリストアが同じスレッドによる別の場所への別のストアの前に、(必ずしも)メモリに書き込まれた最初のデータが2番目のデータよりも前にあることを保証しません。その結果、これらの純粋なメモリアクセスの順序は(必ずしも)「推論」することができず、特に明確に定義されていない限り、何かが発生する可能性があります。
「メモリアクセス」が同期による順序付けに関して明確に定義されている場合、追加のセマンティクスにより、メモリアクセスのタイミングが不確定であっても、同期を通じてその順序を「推論」することができます。メモリアクセス間の順序付けには理由がありますが、必ずしも確定しているわけではないため、競合状態になっていることに注意してください。
2.なぜ違いがあるのですか?
しかし、注文が依然として競合状態で不確定である場合、なぜそれをデータ競合と区別するのが面倒なのでしょうか。その理由は、理論的ではなく実用的です。これは、プログラミング言語とプロセッサアーキテクチャのインターフェイスに違いがあるためです。
モダンアーキテクチャのメモリロード/ストア命令は、アウトオブオーダーパイプライン、スペキュレーション、マルチレベルのキャッシュ、CPU RAM相互接続、特にマルチコアなどの性質により、通常「純粋な」メモリアクセスとして実装されます。 。タイミングと順序が不確定になる要因はたくさんあります。すべてのメモリ命令の順序付けを強制するには、特にマルチコアをサポートするプロセッサデザインでは、大きなペナルティが発生します。そのため、順序付けのセマンティクスには、さまざまなバリア(またはフェンス)などの追加の指示が用意されています。
データ競合は、競合するメモリアクセスの順序付けを推論するのに役立つ追加のフェンスなしのプロセッサ命令実行の状況です。結果は不確定であるだけでなく、非常に奇妙なこともあります。たとえば、異なるスレッドによる同じワードの場所への2つの書き込みは、ワードの半分の書き込みごとに発生するか、ローカルにキャッシュされた値でのみ動作する可能性があります。-これらは、プログラマーの観点から、未定義の動作です。しかし、それらは(通常)プロセッサアーキテクトの観点から明確に定義されています。
プログラマーはコードの実行を推論する方法を持っている必要があります。データの競合は彼らが意味をなさないものなので、常に(通常は)回避すべきです。そのため、十分に低レベルの言語仕様では、データ競合を未定義の動作として定義し、競合状態の明確に定義されたメモリ動作とは異なります。
3.言語記憶モデル
プロセッサが異なれば、メモリアクセス動作も異なります(つまり、プロセッサメモリモデル)。プログラマが現代のすべてのプロセッサのメモリモデルを研究し、それらから利益を得ることができるプログラムを開発するのは厄介です。その言語のプログラムが常にメモリモデルの定義どおりに期待どおりに動作するように、言語でメモリモデルを定義できることが望ましいです。これが、JavaとC ++でメモリモデルが定義されている理由です。言語メモリモデルが異なるプロセッサアーキテクチャに確実に適用されるようにするのは、コンパイラ/ランタイム開発者の負担です。
とは言っても、言語がプロセッサの低レベルの動作を公開したくない場合(そして、現代のアーキテクチャの特定のパフォーマンス上の利点を犠牲にしても構わない場合)、「純粋な」の詳細を完全に隠すメモリモデルを定義することを選択できます。メモリアクセス、ただしすべてのメモリ操作に順序付けセマンティクスを適用します。次に、コンパイラ/ランタイムの開発者は、すべてのプロセッサアーキテクチャですべてのメモリ変数を揮発性として扱うことを選択できます。これらの言語(スレッド間で共有メモリをサポートする)の場合、データの競合はありませんが、完全な順次一貫性のある言語であっても、競合状態になる可能性があります。
一方、プロセッサのメモリモデルは、より厳格(または緩やか、またはより高いレベル)にすることができます。たとえば、初期のプロセッサと同じように逐次一貫性を実装します。次に、すべてのメモリ操作が順序付けられ、プロセッサで実行されている言語のデータ競合は発生しません。
4.まとめ
元の質問に戻りますが、私見では、データ競合を競合状態の特殊なケースとして定義しても問題ありません。あるレベルの競合状態は、より高いレベルのデータ競合になる場合があります。問題の定式化の性質、および未定義の動作と明確に定義されているが不確定な動作との境界をどこに描くかによって異なります。現在の規則だけが言語プロセッサインターフェースで境界を定義していますが、必ずしもそうである必要はありません。しかし、現在の規則はおそらく、プロセッサアーキテクトとプログラミング言語の間の最先端のインターフェース(および知恵)を最もよく反映しています。