Sind C ++ Lese- und Schreibvorgänge eines int Atomic?


80

Ich habe zwei Threads, einen, der ein Int aktualisiert und einen, der es liest. Dies ist ein statistischer Wert, bei dem die Reihenfolge der Lese- und Schreibvorgänge keine Rolle spielt.

Meine Frage ist, muss ich den Zugriff auf diesen Multi-Byte-Wert trotzdem synchronisieren? Oder anders ausgedrückt, kann ein Teil des Schreibvorgangs abgeschlossen sein und unterbrochen werden, und dann geschieht das Lesen.

Stellen Sie sich beispielsweise einen Wert = 0x0000FFFF vor, der den inkrementierten Wert 0x00010000 erhält.

Gibt es eine Zeit, in der der Wert wie 0x0001FFFF aussieht, über die ich mir Sorgen machen sollte? Je größer der Typ, desto wahrscheinlicher ist es, dass so etwas passiert.

Ich habe diese Art von Zugriffen immer synchronisiert, war aber neugierig, was die Community denkt.


5
"Ja wirklich?" Es wäre mir egal, was die Community dachte. Es würde mich interessieren, was die Fakten sind :)
sehe

1
Interessante Lektüre zum Thema: channel9.msdn.com/Shows/Going+Deep/…
ereOn

Antworten:


47

Zuerst könnte man denken, dass Lese- und Schreibvorgänge der nativen Maschinengröße atomar sind, aber es gibt eine Reihe von Problemen, mit denen man sich befassen muss, einschließlich der Cache-Kohärenz zwischen Prozessoren / Kernen. Verwenden Sie atomare Operationen wie Interlocked * unter Windows und das Äquivalent unter Linux. C ++ 0x wird eine "atomare" Vorlage haben, um diese in eine schöne und plattformübergreifende Oberfläche zu packen. Wenn Sie eine Plattformabstraktionsschicht verwenden, bietet diese möglicherweise diese Funktionen. ACE tut dies, siehe die Klassenvorlage ACE_Atomic_Op .


Das Dokument von ACE_Atomic_Op wurde verschoben - es kann jetzt unter dre.vanderbilt.edu/~schmidt/DOC_ROOT/ACE/ace/Atomic_Op.inl
Byron

63

Junge, was für eine Frage. Die Antwort darauf lautet:

Ja, nein, hmmm, es kommt darauf an

Auf die Architektur des Systems kommt es an. Auf einem IA32 ist eine korrekt ausgerichtete Adresse eine atomare Operation. Nicht ausgerichtete Schreibvorgänge können atomar sein. Dies hängt vom verwendeten Caching-System ab. Wenn der Speicher innerhalb einer einzelnen L1-Cache-Zeile liegt, ist er atomar, andernfalls nicht. Die Breite des Busses zwischen CPU und RAM kann die atomare Natur beeinflussen: Ein korrekt ausgerichteter 16-Bit-Schreibvorgang auf einem 8086 war atomar, während der gleiche Schreibvorgang auf einem 8088 nicht darauf zurückzuführen war, dass der 8088 nur einen 8-Bit-Bus hatte, während der 8086 einen hatte 16 Bit Bus.

Wenn Sie C / C ++ verwenden, vergessen Sie nicht, den gemeinsam genutzten Wert als flüchtig zu markieren. Andernfalls glaubt der Optimierer, dass die Variable in einem Ihrer Threads niemals aktualisiert wird.


23
Das flüchtige Schlüsselwort ist in Multithread-Programmen nicht nützlich. Stackoverflow.com/questions/2484980/…

5
@IngeHenriksen: Dieser Link überzeugt mich nicht.
Skizz

eine andere Quelle, aber leider sehr alt (sie ist älter als std :: atomic): web.archive.org/web/20190219170904/https://software.intel.com/…
Max Barraclough

11

Wenn Sie einen 4-Byte-Wert lesen / schreiben UND dieser im Speicher DWORD-ausgerichtet ist UND Sie auf der I32-Architektur arbeiten, sind Lese- und Schreibvorgänge atomar.


2
Wo steht dies in den Handbüchern der Entwickler von Intel-Architektur-Software?
Daniel Trebbien

2
@ DanielTrebbien: vielleicht siehe stackoverflow.com/questions/5002046/…
sehe

9

Ja, Sie müssen Zugriffe synchronisieren. In C ++ 0x handelt es sich um ein Datenrennen und ein undefiniertes Verhalten. Bei POSIX-Threads ist das Verhalten bereits undefiniert.

In der Praxis können schlechte Werte auftreten, wenn der Datentyp größer als die native Wortgröße ist. Außerdem wird in einem anderen Thread der geschriebene Wert möglicherweise nie angezeigt, da Optimierungen das Lesen und / oder Schreiben verschieben.


3

Sie müssen synchronisieren, aber auf bestimmten Architekturen gibt es effiziente Möglichkeiten, dies zu tun.

Verwenden Sie am besten Unterprogramme (möglicherweise hinter Makros maskiert), damit Sie Implementierungen bedingt durch plattformspezifische ersetzen können.

Der Linux-Kernel hat bereits einen Teil dieses Codes.



2

Um das zu wiederholen, was alle oben gesagt haben, kann die Sprache vor C ++ 0x nichts über den Zugriff auf gemeinsam genutzten Speicher von mehreren Threads garantieren. Alle Garantien liegen beim Compiler.


0

Nein, das sind sie nicht (oder zumindest kann man nicht davon ausgehen, dass sie es sind). Allerdings gibt es einige Tricks, um dies atomar zu tun, aber sie sind normalerweise nicht portabel (siehe Vergleichen und Tauschen ).


0

Ich stimme vielen und insbesondere Jason zu . Unter Windows würde man wahrscheinlich InterlockedAdd und seine Freunde verwenden.


0

Abgesehen von dem oben erwähnten Cache-Problem ...

Wenn Sie den Code auf einen Prozessor mit einer kleineren Registergröße portieren, ist er nicht mehr atomar.

IMO, Threading-Probleme sind zu heikel, um es zu riskieren.


0

Die einzige tragbare Möglichkeit besteht darin, den im Header signal.h definierten Typ sig_atomic_t für Ihren Compiler zu verwenden. In den meisten C- und C ++ - Implementierungen ist dies ein int. Deklarieren Sie dann Ihre Variable als "volatile sig_atomic_t".


volatile macht nicht das, was du denkst. stackoverflow.com/questions/2484980/…
Sam Miller

0

Nehmen wir dieses Beispiel

int x;
x++;
x=x+5;

Die erste Anweisung wird als atomar angenommen, da sie in eine einzelne INC-Assembly-Direktive übersetzt wird, die einen einzelnen CPU-Zyklus benötigt. Die zweite Zuweisung erfordert jedoch mehrere Operationen, sodass es sich eindeutig nicht um eine atomare Operation handelt.

Ein anderes zB

x=5;

Auch hier müssen Sie den Code zerlegen, um zu sehen, was genau hier passiert.


2
Aber der Compiler könnte es optimieren x+=6.
tc.

0

tc, ich denke, sobald Sie eine Konstante (wie 6) verwenden, würde die Anweisung nicht in einem Maschinenzyklus abgeschlossen sein. Versuchen Sie, den Befehlssatz von x + = 6 im Vergleich zu x ++ zu sehen


0

Einige Leute denken, dass ++ c atomar ist, haben aber ein Auge auf die erzeugte Assembly. Zum Beispiel mit 'gcc -S':

movl    cpt.1586(%rip), %eax
addl    $1, %eax
movl    %eax, cpt.1586(%rip)

Um ein int zu erhöhen, lädt der Compiler es zuerst in ein Register und speichert es wieder im Speicher. Das ist nicht atomar.


1
Dies ist kein Problem, wenn nur ein Thread in die Variable schreibt, da kein Zerreißen auftritt.
Ben Voigt

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.