Betrachten Sie den folgenden Code:
%Vor% Meine Absicht ist, dass PrevSample
, CurrentSample
und NextSample
konsistent sind, auch wenn gSampleIndex
während des Aufrufs von Process()
aktualisiert wird.
Wird die Zuweisung zum localSampleIndex
den Trick machen, oder besteht die Möglichkeit, dass es weg optimiert wird, obwohl gSampleIndex
flüchtig ist?
In Ihrer Funktion greifen Sie nur einmal auf die volatile
-Variable zu (und es ist die einzige volatile
one in dieser Funktion), so dass Sie sich keine Gedanken über Code-Reorganisation , die der Compiler ausführen kann (und volatile
verhindert). Was Standard für diese Optimierungen in §5.1.2.3 sagt, ist:
In der abstrakten Maschine werden alle Ausdrücke wie in der Semantik angegeben ausgewertet. Eine tatsächliche Implementierung muss einen Teil eines Ausdrucks nicht auswerten, wenn er daraus schließen kann, dass sein Wert nicht verwendet wird und keine erforderlichen Nebenwirkungen erzeugt werden (einschließlich solcher, die durch Aufruf einer Funktion oder Zugriff auf ein flüchtiges Objekt verursacht werden).
Beachten Sie den letzten Satz: "... keine benötigten Nebenwirkungen werden erzeugt (... auf ein flüchtiges Objekt zugreifen)" .
Simply volatile
verhindert, dass ein Optimierungscompiler diesen Code ausführen kann. Um nur einige zu nennen: keine Instruktions-Neuordnung bezüglich anderer volatile
Variablen. kein Ausdruck entfernen, kein Caching, keine Wertweiterleitung über Funktionen.
BTW: Ich bezweifle, dass irgendein Compiler den Code brechen könnte (mit oder ohne volatile
). Möglicherweise wird die lokale Stapelvariable entfernt, aber der Wert wird in einer Registry gespeichert (sicher wird nicht wiederholt auf einen Speicherort zugegriffen). Was Sie brauchen volatile
für ist Wert Sichtbarkeit.
BEARBEITEN
Ich denke, es ist eine Klärung erforderlich.
Lassen Sie mich sicher annehmen, dass Sie wissen, was Sie tun (Sie arbeiten mit Interrupt-Handlern, so sollte dies nicht Ihr erstes C-Programm sein): CPU-Wort stimmt mit Ihrem Variablentyp überein und der Speicher ist richtig ausgerichtet.
Lassen Sie mich auch annehmen, dass Ihr Interrupt nicht reentrant ist (einige magic cli
/ sti
sachen oder was auch immer Ihre CPU dafür verwendet), außer Sie planen ein schweres Debugging und Tuning .
Wenn diese Annahmen erfüllt sind, brauchen Sie keine atomaren Operationen. Warum? Weil localSampleIndex = gSampleIndex
atomar ist (weil es richtig ausgerichtet ist, Wortgröße stimmt und es ist volatile
), gibt es mit ++gSampleIndex
keine Wettlaufbedingung ( HandleSomeIrq
wird nicht erneut aufgerufen, solange es noch ausgeführt wird). Mehr als nutzlos sie sind falsch .
Man könnte denken: "OK, ich brauche vielleicht keinen atomaren, aber warum kann ich sie nicht benutzen? Auch wenn solche Annahmen erfüllt sind, ist das ein * extra * und es wird dasselbe Ziel erreichen." . Nein, tut es nicht . Atomic hat nicht die gleiche Semantik von volatile
Variablen (und selten volatile
ist / sollte außerhalb der Speicherabbildung von E / A und Signalverarbeitung verwendet werden). Flüchtig (normalerweise) ist nutzlos mit atomaren (es sei denn, eine bestimmte Architektur sagt es), aber es hat einen großen Unterschied: Sichtbarkeit . Wenn Sie gSampleIndex
in HandleSomeIrq
standard aktualisieren, wird garantiert, dass der Wert sofort für alle Threads (und Geräte) sichtbar ist. mit atomic_uint Standard garantiert es in einer angemessenen Zeit .
Um es kurz und klar zu machen: flüchtig und atomar sind nicht dasselbe . Atomare Operationen sind nützlich für den Parallelbetrieb, flüchtige sind nützlich für die unteren Ebenen (Interrupts, Geräte). Wenn du immer noch denkst "hey sie tun * genau * was ich brauche" lies bitte einige nützliche Links, die aus Kommentaren ausgewählt wurden: Cache-Kohärenz und eine nette Lektüre über Atomics.
Zusammengefasst:
In Ihrem Fall können Sie eine atomare Variable mit einer Sperre verwenden (um sowohl atomaren Zugriff als auch Wertsichtbarkeit zu haben), aber niemand auf dieser Erde würde eine Sperre in einen Interrupt-Handler setzen (es sei denn, absolut zweifelsfrei zweifellos erforderlich, und aus Code, den Sie gepostet haben) nicht dein Fall).
Im Prinzip ist volatile
nicht genug, um sicherzustellen, dass Process
nur konsistente Werte von gSampleIndex
sieht. In der Praxis sollten jedoch keine Probleme auftreten, wenn uinit32_t
direkt von der Hardware unterstützt wird. Die richtige Lösung wäre die Verwendung atomarer Zugriffe.
Angenommen, Sie verwenden eine 16-Bit-Architektur, so dass der Befehl
lautet %Vor%wird in zwei Anweisungen kompiliert (Laden der oberen Hälfte, Laden der unteren Hälfte). Dann kann der Interrupt zwischen den beiden Anweisungen aufgerufen werden, und Sie erhalten die Hälfte des alten Werts mit der Hälfte des neuen Werts kombiniert.
Die Lösung besteht darin, nur mit atomaren Operationen auf gSampleCounter
zuzugreifen. Ich kenne drei Möglichkeiten, dies zu tun.
In C11 (unterstützt seit GCC 4.9) deklarieren Sie Ihre Variable als atomar:
%Vor%Sie achten darauf, dass Sie immer nur über die dokumentierten atomaren Schnittstellen auf die Variable zugreifen. Im IRQ-Handler:
%Vor% und in der Funktion Process
:
Machen Sie sich nicht die _explicit
-Varianten der atomaren Funktionen zunutze, es sei denn, Sie versuchen, Ihr Programm über eine große Anzahl von Kernen skalieren zu lassen.
Selbst wenn Ihr Compiler C11 noch nicht unterstützt, hat er wahrscheinlich einige Unterstützung für atomare Operationen. Zum Beispiel können Sie in GCC sagen:
%Vor%Wie oben sollten Sie sich nicht mit einer schwachen Konsistenz befassen, es sei denn, Sie versuchen ein gutes Skalierungsverhalten zu erreichen.
Da Sie nicht versuchen, sich gegen den gleichzeitigen Zugriff von mehreren Kernen zu schützen, sondern nur durch Bedingungen mit einem Interrupt-Handler, ist es möglich, ein Konsistenzprotokoll nur unter Verwendung von Standard-C-Primitiven zu implementieren. Der Dekker-Algorithmus ist das älteste bekannte Protokoll.