Ich habe den Wiedereintritt in die Programmierung studiert. Auf dieser Seite von IBM (wirklich gut). Ich habe einen Code gegründet, der unten kopiert wurde. Es ist der erste Code, der auf der Website veröffentlicht wird.
Der Code versucht, die Probleme beim gemeinsamen Zugriff auf Variablen in einer nichtlinearen Entwicklung eines Textprogramms (Asynchronität) aufzuzeigen, indem zwei Werte gedruckt werden, die sich in einem "gefährlichen Kontext" ständig ändern.
#include <signal.h>
#include <stdio.h>
struct two_int { int a, b; } data;
void signal_handler(int signum){
printf ("%d, %d\n", data.a, data.b);
alarm (1);
}
int main (void){
static struct two_int zeros = { 0, 0 }, ones = { 1, 1 };
signal (SIGALRM, signal_handler);
data = zeros;
alarm (1);
while (1){
data = zeros;
data = ones;
}
}
Die Probleme traten auf, als ich versuchte, den Code auszuführen (oder besser gesagt, nicht). Ich habe gcc Version 6.3.0 20170516 (Debian 6.3.0-18 + deb9u1) in der Standardkonfiguration verwendet. Die fehlgeleitete Ausgabe tritt nicht auf. Die Häufigkeit, mit der "falsche" Paarwerte erhalten werden, ist 0!
Was ist denn los? Warum gibt es kein Problem beim Wiedereintritt mit statischen globalen Variablen?
Antworten:
Das ist nicht wirklich Eingang ; Sie führen eine Funktion nicht zweimal im selben Thread (oder in verschiedenen Threads) aus. Sie können dies durch Rekursion oder Übergabe der Adresse der aktuellen Funktion als Rückruffunktionszeigerarg an eine andere Funktion erhalten. (Und es wäre nicht unsicher, weil es synchron wäre).
Dies ist einfach nur Vanilla Data-Race UB (Undefined Behaviour) zwischen einem Signalhandler und dem Haupt-Thread: Nur dies
sig_atomic_t
ist garantiert sicher . Andere funktionieren möglicherweise, wie in Ihrem Fall, in dem ein 8-Byte-Objekt mit einer Anweisung auf x86-64 geladen oder gespeichert werden kann, und der Compiler wählt zufällig diesen asm. (Wie die Antwort von @ icarus zeigt).Siehe MCU-Programmierung - Unterbrechung der C ++ O2-Optimierung während der Schleife - Ein Interrupt-Handler auf einem Single-Core-Mikrocontroller ist im Grunde dasselbe wie ein Signal-Handler in einem Single-Threaded-Programm. In diesem Fall ist das Ergebnis des UB, dass eine Last aus einer Schleife gehoben wurde.
Ihr Testfall des Zerreißens aufgrund eines Datenrassen-UB wurde wahrscheinlich im 32-Bit-Modus oder mit einem älteren dümmeren Compiler entwickelt / getestet, der die Strukturelemente separat geladen hat.
In Ihrem Fall kann der Compiler die Speicher aus der Endlosschleife heraus optimieren, da kein UB-freies Programm sie jemals beobachten könnte.
data
ist nicht_Atomic
odervolatile
, und es gibt keine anderen Nebenwirkungen in der Schleife. Es gibt also keine Möglichkeit, dass ein Leser mit diesem Schreiber synchronisiert. Dies geschieht tatsächlich, wenn Sie mit aktivierter Optimierung kompilieren ( Godbolt zeigt eine leere Schleife am unteren Rand von main). Ich habe auch die Struktur in zwei geändertlong long
, und gcc verwendet einen einzelnenmovdqa
16-Byte-Speicher vor der Schleife. (Dies ist nicht garantiert atomar, aber es ist in der Praxis auf fast allen CPUs, vorausgesetzt, es ist ausgerichtet, oder Intel überschreitet lediglich keine Cache-Zeilengrenze. Warum ist die Ganzzahlzuweisung für eine natürlich ausgerichtete Variable atomar auf x86? )Das Kompilieren mit aktivierter Optimierung würde also auch Ihren Test unterbrechen und Ihnen jedes Mal den gleichen Wert anzeigen. C ist keine tragbare Assemblersprache.
volatile struct two_int
würde den Compiler auch zwingen, sie nicht zu optimieren, würde ihn aber nicht zwingen, die gesamte Struktur atomar zu laden / speichern. (Es wäre nicht aufhören es von so entweder zu tun, wenn.) Beachten Sie, dassvolatile
sich keine Daten-Rennen UB vermeiden, aber es ist ausreichend für inter-thread Kommunikation in der Praxis und war , wie die Menschen von Hand gerollt atomics gebaut (zusammen mit Inline - asm) vor C11 / C ++ 11 für normale CPU-Architekturen. Sie sind Cache-kohärente sovolatile
ist in der Praxis meist ähnlich wie_Atomic
mitmemory_order_relaxed
rein Last und pure-Speicher, wenn für Typen verwendeten schmal genug , dass der Compiler einen einzigen Befehl verwenden , damit Sie nicht Zerreißen erhalten. Und natürlichvolatile
Es gibt keine Garantien des ISO C-Standards für das Schreiben von Code, der mit_Atomic
und mo_relaxed auf dieselbe Weise kompiliert wird.Wenn Sie eine Funktion hätten, die
global_var++;
auf einemint
oder von einem Signalhandlerlong long
aus asynchron und asynchron ausgeführt wird, wäre dies eine Möglichkeit, die erneute Eingabe zum Erstellen eines Datenrassen-UB zu verwenden.Abhängig davon, wie es kompiliert wurde (zu einem Speicherziel inkl. Hinzufügen oder hinzufügen oder zum Laden / Inkl / Speichern), wäre es in Bezug auf Signalhandler im selben Thread atomar oder nicht. Siehe Kann num ++ für 'int num' atomar sein? Weitere Informationen zur Atomizität unter x86 und in C ++. (C11s
stdatomic.h
und_Atomic
Attribute bieten äquivalente Funktionen wie diestd::atomic<T>
Vorlage von C ++ 11 )Ein Interrupt oder eine andere Ausnahme kann nicht in der Mitte eines Befehls auftreten, daher ist das Hinzufügen eines Speicherziels atomar. Kontext schaltet eine Single-Core-CPU ein. Nur ein (Cache-kohärenter) DMA-Writer kann ein Inkrement von a
add [mem], 1
ohnelock
Präfix auf einer Single-Core-CPU "betreten". Es gibt keine anderen Kerne, auf denen ein anderer Thread ausgeführt werden könnte.Es ist also ähnlich wie bei Signalen: Ein Signalhandler wird anstelle der normalen Ausführung des Threads ausgeführt, der das Signal verarbeitet, sodass er nicht in der Mitte eines Befehls verarbeitet werden kann.
quelle
Wenn man sich den Godbolt- Compiler-Explorer ansieht (nachdem man den fehlenden hinzugefügt hat
#include <unistd.h>
), sieht man, dass für fast jeden x86_64-Compiler der generierte Code QWORD-Verschiebungen verwendet, um dasones
undzeros
in einer einzelnen Anweisung zu laden .Auf der IBM Website wird angegeben,
On most machines, it takes several instructions to store a new value in data, and the value is stored one word at a time.
was für einen typischen CPU im Jahr 2005 möglicherweise zutraf, aber wie der Code zeigt, ist dies derzeit nicht der Fall. Das Ändern der Struktur auf zwei Longs anstelle von zwei Ints würde das Problem anzeigen.Ich habe vorher geschrieben, dass dies "atomar" war, was faul war. Das Programm läuft nur auf einer einzigen CPU. Jede Anweisung wird aus der Sicht dieser CPU abgeschlossen (vorausgesetzt, es gibt nichts anderes, was den Speicher verändert, wie z. B. dma).
Auf der
C
Ebene ist also nicht definiert, dass der Compiler eine einzelne Anweisung zum Schreiben der Struktur auswählt, sodass die im IBM Dokument erwähnte Beschädigung auftreten kann. Moderne Compiler, die auf aktuelle CPUs abzielen, verwenden eine einzige Anweisung. Eine einzelne Anweisung ist gut genug, um eine Beschädigung für ein einzelnes Thread-Programm zu vermeiden.quelle
int
zu ändernlong long
und auf 32 Bit zu kompilieren. Die Lektion ist, dass Sie nie wissen, ob / wann es brechen wird.long long
Kompiliert immer noch zu einem Befehl für x86-64: 16-Bytemovdqa
. Es sei denn, Sie deaktivieren die Optimierung wie in Ihrem Godbolt-Link. (Die Standardeinstellung von GCC ist der-O0
Debug-Modus, der voller Speicher- / Neuladgeräusche ist und normalerweise nicht interessant anzusehen ist.)