2012-04-03 13 views
2

Ich habe eine Frage zu Race Conditions und simultanen Schreiben.Race condition und entriegelt schreiben

Ich habe eine Klasse, auf die Objekte aus verschiedenen Threads zugegriffen werden. Ich möchte einige Werte nur bei Bedarf berechnen und das Ergebnis zwischenspeichern. Aus Performance-Gründen würde ich lieber keine Schlösser benutzen (bevor jemand fragt - ja, das ist in meinem Fall relevant).

Dies ist eine Race-Bedingung. Die Objekte sind jedoch konstant und werden nicht geändert. Wenn also unterschiedliche Threads Werte berechnen, die zwischengespeichert werden sollen, sind sie in meinem Anwendungsfall garantiert identisch. Wäre es sicher, diese Werte ohne Sperren zu schreiben? Oder ist es im weiteren Sinne sicher, identische Inhalte aus verschiedenen Threads in den Speicher zu schreiben, ohne zu sperren?

Die geschriebenen Werte sind vom Typ bool und double und die betreffenden Architekturen können x86 und ARM sein.

EDIT: Danke an alle für ihre Eingabe. Ich habe mich schließlich entschieden, einen Weg zu finden, der kein Caching beinhaltet. Dieser Ansatz scheint einem "Hack" sehr ähnlich zu sein, und es gibt das Problem, eine Flag-Variable zu verwenden.

+0

Ich bezweifle, dass C++ es garantiert, aber zumindest mit moderner Hardware ist es schwer vorstellbar, wie ein Problem entstehen würde. –

+0

Jede Schreiboperation für ein Primitiv ist atomar. Es sollte also keinen Unterschied machen, ob viele Threads gleichzeitig in dieselbe Gruppe von Feldern schreiben. – Alain

+1

"Wenn also verschiedene Threads Werte berechnen, die zwischengespeichert werden sollen, sind sie in meinem Anwendungsfall garantiert identisch." ... Das ist seltsam. Wenn Sie garantieren können, dass die geschriebenen Werte identisch sind, warum sollten Sie sie gleichzeitig schreiben? Oder besser, warum schreibe ich sie mehr als einmal? –

Antwort

4

Wie Sie sagen, ist dies eine Race-Bedingung. Unter C++ 11 ist es technisch ein Datenrennen, undefiniertes Verhalten. Es spielt keine Rolle, dass die Werte gleich sind.

Wenn Ihr Compiler unterstützt (zB der letzte gcc oder gcc oder MSVC mit meiner Just::Thread Bibliothek), dann können Sie std::atomic<some_pod_struct> verwenden, um eine Atom Wrapper um Ihre Daten zu liefern (vorausgesetzt, es ist eine POD-Struktur --- wenn es Dann hast du größere Probleme). Wenn es klein genug ist, macht der Compiler es frei von Sperren und verwendet die entsprechenden atomaren Operationen. Bei größeren Strukturen wird die Bibliothek eine Sperre verwenden.

Das Problem bei der Ausführung ohne atomare Operationen oder Sperren ist Sichtbarkeit. Während es auf der Prozessor-Ebene auf x86 oder ARM kein Problem gibt, dieselben Daten zu schreiben (vorausgesetzt, es ist tatsächlich Byte für Byte identisch) von mehreren Threads/Prozessoren zu demselben Speicher, vorausgesetzt, dass dies ein Cache ist, erwarte ich Sie sollten diese Daten lesen, anstatt sie neu zu berechnen, wenn sie bereits geschrieben wurden. Du brauchst also irgendeine Art von Flagge, um auf Done-ness hinzuweisen. Wenn Sie keine atomaren Operationen, Sperren oder geeignete Speicherbarrierenanweisungen verwenden, kann das Flag "ready" für einen anderen Prozessor vor den Daten sichtbar werden. Dies wird dann wirklich Dinge vermasseln, da der zweite Prozessor nun einen unvollständigen Datensatz liest.

Sie könnten die Daten mit nicht atomaren Operationen schreiben und dann einen atomaren Datentyp für das Flag verwenden. Unter C++ 11 erzeugt dies geeignete Speicherbarrieren und Synchronisation, um sicherzustellen, dass die Daten für jeden Thread sichtbar sind, der das Flag gesetzt sieht. Es ist immer noch undefiniertes Verhalten für zwei Threads, um die Daten zu schreiben, aber es kann in der Praxis OK sein.

Speichern Sie die Daten alternativ in einem Heapspeicherblock, der von jedem Thread zugewiesen wird, der die Berechnung durchführt, und verwenden Sie eine compare-and-swap-Operation, um eine atomare Zeigervariable festzulegen. Wenn das Vergleichen und Tauschen fehlschlägt, ist ein anderer Thread zuerst dort gelandet, also befreien Sie die Daten.

+0

Ausgewählt als beste Antwort für das Aufzeigen des Problems mit einer Flag-Variable und die Notwendigkeit einer Speicherbarriere. –

+0

@Anthony, +1 für die technische Bedeutung des Datenrennens in C++ gegenüber dem allgemeinen Konzept der Rennbedingung. C++ gibt nur an, welches Datenrennen und welches UB gerade stattfindet. Wenn also eine gemeinsam genutzte Variable ein C++ 11-atomarer Typ ist, ist garantiert, dass sie datenrennenfrei ist, selbst wenn sie mit der Reihenfolge memory_order_relaxed getaggt ist. Der Standard erwähnt das allgemeine Race-Condition-Konzept nicht und sagt nicht, dass es UB gibt, wenn das passiert. Es hängt vollständig von den Programmierern ab, Property-Sync-Primitive hinzuzufügen, um die korrekte Semantik sicherzustellen. Sind Sie einverstanden? –

+0

@Anthony, es gibt einige andere glauben, dass es noch Datenrennen in C++ 11 gibt, selbst wenn die geteilten Daten atomaren Typs mit entspannter Reihenfolge sind. Es fällt mir schwer, diesen Begriffen (in der Sprache und in Ihrem Buch richtig definiert) zu erklären. Könnten Sie sich bitte [diesen Beitrag] (https://groups.google.com/forum/#!topic/comp.lang.c++.moderated/1SN85LRbouw) ansehen und uns Ihre Meinung mitteilen? Vielen Dank. –

1

Letztendlich hängt die Antwort wahrscheinlich von Ihrer Datenstruktur ab.

Im Bereich "nicht portierbar" möchten Sie vielleicht in compare and swap schauen, was die meisten Prozessoren für eine zeigergroße Entität zulassen. Um darauf zuzugreifen, können Sie Inline Assembly verwenden (auf x86 sind dies die lock cmpxchg Anweisungen), oder vielleicht GCC Synchronisationserweiterungen. Wenn ein nicht initialisierter Wert angezeigt wird, könnte jeder Thread eifrig initialisiert werden und einen Vergleichs- und Austauschvorgang ausführen, um einen Wert festzulegen. Wenn das Vergleichen und Tauschen fehlschlägt, bedeutet das, dass ein anderer Thread Sie dazu geschlagen hat.

Letztlich Nutzung dieser Operation endet oft obwohl ein spinlock ist das Äquivalent der Implementierung, die Sie zu vermeiden suchen können ...

+0

Ein Teil der Datenstruktur ist ein 8-Byte-Double-on-ARM (32-Bit), also sind sie größer als die "pointergroße Entität". Das wäre also keine Option. –

+0

@GabrielSschreiber - Das bedeutet nicht, dass es keine Option ist. Zum Beispiel könnten Sie einen Zeiger auf die Struktur anstelle der Struktur selbst initialisieren. Oder Sie könnten ein "Companion" -Wort haben, das im Wesentlichen als Sperre für die Initialisierung dient - um festzustellen, ob es zuvor initialisiert wurde.Allerdings würde ich Sie im Allgemeinen dazu ermutigen, vorsichtig zu sein, wenn Sie Dinge als "atomar" bezeichnen, wenn sie sich darauf verlassen, mehrere Wörter gleichzeitig zu schreiben. – asveikau

+0

@GabrielSschreiber - Ich muss betonen, dass, wenn Sie wollen, dass Lese-Modify-Write-Operationen atomare, Vergleiche und Swap (oder die plattformspezifische Äquivalent; "Load-Link/Store-Conditional" ist die RISC-Version) schön ist viel der einzige Weg zu gehen. – asveikau

1

Sie brauchen nicht von verschiedenen Threads gegen schreibt zu POD Variablen zu schützen wenn die Werte gleich wären. Wenn Sie jedoch Zeiger haben, sollten Sie unbedingt einen verzahnten Austausch durchführen.

UPDATE: Um zu klären, für Ihre Situation haben Caching und Optimierungen keine nachteiligen Auswirkungen, da Sie den gleichen Wert auf alle Threads schreiben. Aus dem gleichen Grund müssen Sie die Variable volatile nicht erstellen. Das einzige Problem, das möglicherweise ein Problem darstellt, ist, wenn Ihre Variable nicht mit der Wortgröße des Computers übereinstimmt. Weitere Details finden Sie unter https://stackoverflow.com/a/54242/677131. Standardmäßig werden Variablen automatisch ausgerichtet, Sie können die Ausrichtung jedoch explizit ändern.

Es gibt einen alternativen Ansatz, der dieses Problem vollständig vermeidet. Da die Variablen die gleichen Werte haben, rechnen Sie sie entweder vor der gleichzeitigen Ausführung vor oder lassen Sie jeden Thread seine eigene Kopie erstellen. Letzteres hat den Vorteil, dass es eine bessere Leistung auf NUMA-Maschinen bietet.

1

Zunächst muss ich sagen, dass Verriegelung, ist in der Regel der richtige Weg, dies zu tun, aber ...

auf die gleiche Variable von mehreren Threads Schreiben nicht unsicher sein kann, selbst wenn die Daten größer als Prozessor Wort Größe. Es gibt keinen Übergangszustand, in dem die Variable beschädigt werden kann, da mindestens ein Thread den Wert abschließt. Andere Threads ändern es nicht, indem sie über den gleichen Wert wringen.

Wenn also sichergestellt ist, dass die Berechnungsergebnisse immer gleich sind, egal welcher Thread, besteht keine Gefahr, dass dies durch mehrere Threads geschieht.Überprüfen Sie einfach ein Flag ("ist bereits berechnet?") Vor der Berechnung. Mehrere Threads geben den Code für den Wert ein, aber sobald das erledigt ist, werden das natürlich keine anderen Threads mehr tun. Natürlich ist es eine Zeitverschwendung, n-mal dasselbe zu tun. Frage ist hier, wird die Verwendung eines Schlosses Sie jederzeit oder im Gegenteil speichern? Nur Leistungstests können Ihnen die Antwort geben. Es sei denn, es gibt andere Gründe, Sperren nicht zu verwenden.