Sind "Datenrennen" und "Rennbedingungen" im Kontext der gleichzeitigen Programmierung tatsächlich dasselbe?

Antworten:

135

Nein, sie sind nicht dasselbe. Sie sind keine Teilmenge voneinander. Sie sind auch weder die notwendige noch die ausreichende Voraussetzung für einander.

Die Definition eines Datenrennens ist ziemlich klar und daher kann seine Erkennung automatisiert werden. Ein Datenwettlauf tritt auf, wenn zwei Anweisungen von verschiedenen Threads auf denselben Speicherort zugreifen. Mindestens einer dieser Zugriffe ist ein Schreibvorgang, und es gibt keine Synchronisation, die eine bestimmte Reihenfolge zwischen diesen Zugriffen vorschreibt .

Eine Rennbedingung ist ein semantischer Fehler. Es ist ein Fehler, der im Timing oder in der Reihenfolge der Ereignisse auftritt und zu einem fehlerhaften Programmverhalten führt. Viele Rennbedingungen können durch Datenrennen verursacht werden, dies ist jedoch nicht erforderlich.

Betrachten Sie das folgende einfache Beispiel, in dem x eine gemeinsam genutzte Variable ist:

Thread 1    Thread 2

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

In diesem Beispiel sind die Schreibvorgänge von Thread 1 und 2 in x durch Sperren geschützt. Daher erfolgen sie immer in einer Reihenfolge, die durch die Reihenfolge erzwungen wird, in der die Sperren zur Laufzeit erfasst werden. Das heißt, die Atomizität der Schriften kann nicht gebrochen werden; Es gibt immer ein Ereignis, bevor die Beziehung zwischen den beiden Schreibvorgängen in einer Ausführung erfolgt. Wir können einfach nicht wissen, welches Schreiben a priori vor dem anderen stattfindet.

Es gibt keine feste Reihenfolge zwischen den Schreibvorgängen, da Sperren dies nicht bereitstellen können. Wenn die Korrektheit der Programme beeinträchtigt ist, z. B. wenn auf das Schreiben von Thread 2 auf x gefolgt von dem Schreiben auf x in Thread 1 folgt, liegt eine Racebedingung vor, obwohl technisch gesehen kein Datenrennen vorliegt.

Es ist weitaus nützlicher, Rennbedingungen zu erkennen als Datenrennen. Dies ist jedoch auch sehr schwer zu erreichen.

Das umgekehrte Beispiel zu konstruieren ist ebenfalls trivial. Dieser Blog-Beitrag erklärt den Unterschied auch sehr gut anhand eines einfachen Beispiels für Bankgeschäfte.

Baris Kasikci
quelle
"Datenrennen (...) keine Synchronisation, die eine bestimmte Reihenfolge zwischen diesen Zugriffen vorschreibt." Ich bin etwas verwirrt. In Ihrem Beispiel können die Operationen in beiden Reihenfolgen auftreten (entweder = 1, dann = 2 oder umgekehrt). Warum ist es kein Datenrennen?
Josinalvo
5
@josinalvo: Es ist ein Artefakt der technischen Definition eines Datenrennens. Der entscheidende Punkt ist, dass zwischen den beiden Zugriffen eine Sperrenfreigabe und eine Sperrenerfassung erfolgen (für eine der möglichen Bestellungen). Per Definition stellen eine Sperrenfreigabe und eine Sperrenerfassung eine Reihenfolge zwischen den beiden Zugriffen her, und daher gibt es kein Datenrennen.
Baris Kasikci
Die Synchronisierung erfordert niemals eine bestimmte Reihenfolge zwischen Operationen, daher ist dies keine sehr glückliche Möglichkeit, dies auszudrücken. Andererseits gibt das JMM an, dass es für jede Leseoperation einen bestimmten Schreibvorgang geben muss, den es selbst in einem Datenrennen beobachtet. Es ist schwer zu vermeiden, dass die Reihenfolge " Vorher" und "Synchronisieren" explizit erwähnt wird , aber selbst die JLS-Definition ist falsch, wenn "Nur passiert" erwähnt wird : Nach ihrer Definition bilden zwei gleichzeitige flüchtige Schreibvorgänge ein Datenrennen.
Marko Topolnik
@BarisKasikci "stellt eine Bestellung auf" hat für mich keine wirkliche Bedeutung. Es sind nur Wieselwörter. Ich glaube ehrlich gesagt nicht, dass "Datenrennen" ein entfernt nützliches Konzept ist, da buchstäblich jeder Speicherort, auf den mehrere Threads zugreifen, als "
Datenrennen
Release-Acquisition-Paare legen immer eine Bestellung fest. Eine allgemeine Erklärung ist langwierig, aber ein triviales Beispiel ist ein Signal-Warte-Paar. @Noldorin "Etabliert eine Bestellung" bezieht sich auf eine Vor-Vor-Reihenfolge, die ein Schlüsselkonzept der Parallelitätstheorie (siehe Lamports wegweisendes Papier über die Vor-Vor-Beziehung) und verteilte Systeme ist. Datenrennen sind insofern ein nützliches Konzept, als ihre Anwesenheit viele Probleme aufwirft (z. B. undefinierte Semantik gemäß C ++ - Speichermodell, sehr komplexe Semantik in Java usw.). Ihre Entdeckung und Beseitigung bilden eine umfangreiche Literatur in Forschung und Praxis.
Baris Kasikci
20

Laut Wikipedia wird der Begriff "Race Condition" seit den Tagen der ersten elektronischen Logikgatter verwendet. Im Kontext von Java kann sich eine Race-Bedingung auf eine beliebige Ressource beziehen, z. B. eine Datei, eine Netzwerkverbindung, einen Thread aus einem Thread-Pool usw.

Der Begriff "Datenrennen" ist am besten seiner spezifischen Bedeutung vorbehalten, die vom JLS definiert wird .

Der interessanteste Fall ist eine Rennbedingung, die einem Datenrennen sehr ähnlich ist, aber immer noch keine ist, wie in diesem einfachen Beispiel:

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

Da ies volatil ist, gibt es kein Datenrennen; Unter dem Gesichtspunkt der Programmkorrektheit gibt es jedoch eine Racebedingung aufgrund der Nichtatomarität der beiden Operationen: Lesen i, Schreiben i+1. Mehrere Threads können denselben Wert von erhalten uniqueInt.

Marko Topolnik
quelle
1
Können Sie in Ihrer Antwort eine Zeile einfügen, die beschreibt, was data racein JLS tatsächlich bedeutet?
Geek
@geek Das Wort "JLS" ist ein Hyperlink zum entsprechenden Abschnitt des JLS.
Marko Topolnik
@ MarkoTopolnik Ich bin wenig verwirrt von dem Beispiel. Könnten Sie bitte erklären: "Da ich volatil bin, gibt es kein Datenrennen"? Die Volatilität stellte nur sicher, dass es sichtbar ist, aber dennoch: 1) es ist nicht synchronisiert und mehrere Threads können gleichzeitig lesen / schreiben und 2) es wird ein nicht endgültiges Feld geteilt. Es ist ein Datenrennen und keine Rennbedingung, nicht wahr?
Aniliitb10
@ aniliitb10 Anstatt sich auf gebrauchte Zitate zu verlassen, die aus ihrem Kontext herausgerissen wurden, sollten Sie den JLS-Abschnitt 17.4 lesen, auf den ich in meiner Antwort verwiesen habe. Der Zugriff auf eine flüchtige Variable ist eine Synchronisationsaktion gemäß §17.4.2.
Marko Topolnik
@ aniliitb10 Votaltiles verursachen kein Datenrennen, da ihre Zugriffe bestellt werden können. Das heißt, Sie können ihre Reihenfolge auf diese oder jene Weise begründen, was zu unterschiedlichen Ergebnissen führt. Bei Data Race haben Sie keine Möglichkeit, die Bestellung zu begründen. Beispielsweise kann die i ++ - Operation jedes Threads nur bei ihrem jeweiligen lokal zwischengespeicherten Wert i erfolgen. Weltweit haben Sie keine Möglichkeit, diese Vorgänge zu ordnen (aus Sicht des Programmierers) - es sei denn, Sie verfügen über ein bestimmtes Sprachgedächtnismodell.
Xiao-Feng Li
3

Nein, sie sind unterschiedlich und keiner von ihnen ist eine Teilmenge von eins oder umgekehrt.

Der Begriff Race Condition wird häufig mit dem zugehörigen Term Data Race verwechselt, der auftritt, wenn die Synchronisation nicht verwendet wird, um den gesamten Zugriff auf ein gemeinsam genutztes nicht endgültiges Feld zu koordinieren. Sie riskieren ein Datenrennen, wenn ein Thread eine Variable schreibt, die möglicherweise als nächstes von einem anderen Thread gelesen wird, oder eine Variable liest, die möglicherweise zuletzt von einem anderen Thread geschrieben wurde, wenn beide Threads keine Synchronisation verwenden. Code mit Datenrassen hat keine nützliche definierte Semantik unter dem Java-Speichermodell. Nicht alle Rennbedingungen sind Datenrennen, und nicht alle Datenrennen sind Rennbedingungen, aber beide können dazu führen, dass gleichzeitige Programme auf unvorhersehbare Weise fehlschlagen.

Entnommen aus dem hervorragenden Buch - Java Concurrency in Practice von Joshua Bloch & Co.

Shirgill Farhan
quelle
Beachten Sie, dass die Frage ein sprachunabhängiges Tag hat.
Martinkunev
1

TL; DR: Die Unterscheidung zwischen Datenrennen und Rennbedingungen hängt von der Art der Problemformulierung ab und davon, wo die Grenze zwischen undefiniertem Verhalten und genau definiertem, aber unbestimmtem Verhalten gezogen werden soll. Die derzeitige Unterscheidung ist konventionell und spiegelt am besten die Schnittstelle zwischen Prozessorarchitekt und Programmiersprache wider.

1. Semantik

Datenrennen bezieht sich speziell auf die nicht synchronisierten widersprüchlichen "Speicherzugriffe" (oder Aktionen oder Operationen) auf denselben Speicherort. Wenn es keinen Konflikt bei den Speicherzugriffen gibt, während es immer noch ein unbestimmtes Verhalten gibt, das durch die Reihenfolge der Operationen verursacht wird, ist dies eine Racebedingung.

Hinweis "Speicherzugriffe" haben hier eine bestimmte Bedeutung. Sie beziehen sich auf die "reinen" Speicherlade- oder Speicheraktionen, ohne dass zusätzliche Semantik angewendet wird. Beispielsweise weiß ein Speicher eines Threads (nicht unbedingt), wie lange es dauert, bis die Daten in den Speicher geschrieben sind, und wird schließlich an einen anderen Thread weitergegeben. In einem anderen Beispiel garantiert ein Speicher an einem Speicherort vor einem anderen Speicher an einem anderen Speicherort durch denselben Thread nicht (notwendigerweise), dass die ersten in den Speicher geschriebenen Daten vor den zweiten liegen. Infolgedessen kann die Reihenfolge dieser reinen Speicherzugriffe nicht (notwendigerweise) "begründet" werden , und es kann alles passieren, sofern nichts anderes genau definiert ist.

Wenn die "Speicherzugriffe" in Bezug auf die Reihenfolge durch Synchronisation gut definiert sind, kann eine zusätzliche Semantik sicherstellen, dass ihre Reihenfolge durch die Synchronisationen "begründet" werden kann, selbst wenn der Zeitpunkt der Speicherzugriffe unbestimmt ist . Beachten Sie, dass die Reihenfolge zwischen den Speicherzugriffen zwar begründet werden kann, sie jedoch nicht unbedingt bestimmt sind, daher die Race-Bedingung.

2. Warum der Unterschied?

Aber wenn die Reihenfolge im Rennzustand noch unbestimmt ist, warum sollte man sich dann die Mühe machen, sie vom Datenrennen zu unterscheiden? Der Grund liegt eher in der Praxis als in der Theorie. Dies liegt daran, dass in der Schnittstelle zwischen Programmiersprache und Prozessorarchitektur unterschieden wird.

Ein Speicherlade- / Speicherbefehl in einer modernen Architektur wird normalerweise als "reiner" Speicherzugriff implementiert, aufgrund der Art der Pipeline außerhalb der Reihenfolge, der Spekulation, der Cache-Ebene auf mehreren Ebenen, der CPU-RAM-Verbindung, insbesondere der Multi-Core-Verbindung usw. Es gibt viele Faktoren, die zu einem unbestimmten Zeitpunkt und einer unbestimmten Reihenfolge führen. Das Erzwingen der Reihenfolge für jeden Speicherbefehl ist mit einem enormen Nachteil verbunden, insbesondere bei einem Prozessordesign, das Multi-Core unterstützt. Daher wird die Ordnungssemantik mit zusätzlichen Anweisungen wie verschiedenen Barrieren (oder Zäunen) versehen.

Datenrennen ist die Situation der Ausführung von Prozessorbefehlen ohne zusätzliche Zäune, um die Reihenfolge widersprüchlicher Speicherzugriffe zu begründen. Das Ergebnis ist nicht nur unbestimmt, sondern möglicherweise auch sehr seltsam, z. B. können zwei Schreibvorgänge an dieselbe Wortposition durch unterschiedliche Threads mit jeder Schreibhälfte des Wortes resultieren oder nur mit ihren lokal zwischengespeicherten Werten arbeiten. - Dies ist aus Sicht des Programmierers undefiniertes Verhalten. Aber sie sind (normalerweise) aus Sicht des Prozessorarchitekten gut definiert.

Programmierer haben einen Weg haben , um die Vernunft ihre Code - Ausführung. Datenrennen sind etwas, das sie nicht sinnvoll machen können, daher sollten sie (normalerweise) immer vermieden werden. Aus diesem Grund definieren die Sprachspezifikationen, die niedrig genug sind, Datenrennen normalerweise als undefiniertes Verhalten, das sich vom genau definierten Speicherverhalten der Rassenbedingungen unterscheidet.

3. Sprachgedächtnismodelle

Unterschiedliche Prozessoren können ein unterschiedliches Speicherzugriffsverhalten aufweisen, dh ein Prozessorspeichermodell. Für Programmierer ist es schwierig, das Speichermodell jedes modernen Prozessors zu studieren und dann Programme zu entwickeln, die von ihnen profitieren können. Es ist wünschenswert, wenn die Sprache ein Speichermodell definieren kann, so dass sich die Programme dieser Sprache immer wie erwartet verhalten, wie es das Speichermodell definiert. Aus diesem Grund sind in Java und C ++ die Speichermodelle definiert. Es liegt in der Verantwortung der Compiler- / Laufzeitentwickler, sicherzustellen, dass die Sprachspeichermodelle über verschiedene Prozessorarchitekturen hinweg erzwungen werden.

Das heißt, wenn eine Sprache das Verhalten des Prozessors auf niedriger Ebene nicht offenlegen möchte (und bereit ist, bestimmte Leistungsvorteile der modernen Architekturen zu opfern), können sie ein Speichermodell definieren, das die Details von "rein" vollständig verbirgt. Speicherzugriffe, aber Anwenden der Ordnungssemantik für alle Speicheroperationen. Dann können die Compiler- / Laufzeitentwickler entscheiden, jede Speichervariable in allen Prozessorarchitekturen als flüchtig zu behandeln. Für diese Sprachen (die gemeinsam genutzten Speicher über Threads hinweg unterstützen) gibt es keine Datenrennen, es können jedoch auch bei einer Sprache mit vollständiger sequentieller Konsistenz Rennbedingungen vorliegen.

Andererseits kann das Prozessorspeichermodell strenger (oder weniger entspannt oder auf einer höheren Ebene) sein, z. B. indem eine sequentielle Konsistenz implementiert wird, wie dies bei frühen Prozessoren der Fall war. Dann werden alle Speicheroperationen geordnet, und es gibt kein Datenrennen für Sprachen, die im Prozessor ausgeführt werden.

4. Fazit

Zurück zur ursprünglichen Frage, IMHO ist es in Ordnung, Datenrennen als Sonderfall der Rennbedingung zu definieren, und Rennbedingungen auf einer Ebene können zu Datenrennen auf einer höheren Ebene werden. Dies hängt von der Art der Problemformulierung ab und davon, wo die Grenze zwischen undefiniertem Verhalten und genau definiertem, aber unbestimmtem Verhalten gezogen werden soll. Nur die aktuelle Konvention definiert die Grenze an der Sprach-Prozessor-Schnittstelle, bedeutet nicht unbedingt, dass dies immer der Fall ist und sein muss; Die derzeitige Konvention spiegelt jedoch wahrscheinlich am besten die hochmoderne Schnittstelle (und Weisheit) zwischen Prozessorarchitekt und Programmiersprache wider.

Xiao-Feng Li
quelle