2010-03-19 14 views
17

Ich möchte überprüfen, ob mein Verständnis korrekt ist. So etwas ist schwierig, also bin ich mir fast sicher, dass mir etwas fehlt. Ich habe ein Programm, das aus einem Echtzeit-Thread und einem Nicht-Echtzeit-Thread besteht. Ich möchte, dass der Nicht-RT-Thread einen Zeiger auf Speicher austauschen kann, der vom RT-Thread verwendet wird.Atomic Swap in GNU C++

Aus den Dokumenten, mein Verständnis ist, dass dies in g++ mit erreicht werden kann:

// global 
Data *rt_data; 

Data *swap_data(Data *new_data) 
{ 
#ifdef __GNUC__ 
    // Atomic pointer swap. 
    Data *old_d = __sync_lock_test_and_set(&rt_data, new_data); 
#else 
    // Non-atomic, cross your fingers.           
    Data *old_d = rt_data; 
    rt_data = new_data; 
#endif 
    return old_d; 
} 

Dies ist die einzige Stelle im Programm (andere als Ersteinrichtung), wo rt_data modifiziert wird. Wenn rt_data im Echtzeitkontext verwendet wird, wird es in einen lokalen Zeiger kopiert. Für old_d, später, wenn es sicher ist, dass der alte Speicher nicht verwendet wird, wird es im Nicht-RT-Thread freigegeben. Ist das richtig? Benötige ich volatile irgendwo? Gibt es andere Synchronisationsprimitive, die ich aufrufen sollte?

Durch die Art, wie ich dies in C++ tue, obwohl ich interessiert bin, ob die Antwort für C. Zeit

Dank voraus unterscheidet.

Antwort

24

Im Allgemeinen volatile nicht verwenden, wenn gleichzeitig Code in C/C++ geschrieben wird.Die Semantik von volatile ist so nah an dem, was Sie wollen, dass es verlockend ist, aber am Ende volatil ist not enough. Leider Java/C# volatile != C/C++ volatile. Herb Sutter hat eine tolle article, die das verwirrende Chaos erklärt.

Was Sie wirklich wollen, ist ein Speicherzaun. __sync_lock_test_and_set bietet das Fechten für Sie.

Sie benötigen auch einen Speicherzaun, wenn Sie den Zeiger rt_data auf Ihre lokale Kopie kopieren (laden).

Lock-freie Programmierung ist schwierig. Wenn Sie bereit sind, Gcc C++ 0x-Erweiterungen zu verwenden, ist es ein bisschen einfacher:

#include <cstdatomic> 

std::atomic<Data*> rt_data; 

Data* swap_data(Data* new_data) 
{ 
    Data* old_data = rt_data.exchange(new_data); 
    assert(old_data != new_data); 
    return old_data; 
} 

void use_data() 
{ 
    Data* local = rt_data.load(); 
    /* ... */ 
} 
+0

Vielen Dank! Ich kann Ihrem Vorschlag von std :: atomic folgen, das ist ausgezeichnet. (Ich bin mit den neuesten C++ 0x-Sachen noch nicht gut vertraut.) Nur aus Neugierde, wenn ich __sync_lock_test_and_set verwende, was ist der richtige Zaun, der beim Lesen verwendet wird? (d. h. um eine lokale Kopie zu erstellen) – Steve

3

aktualisieren: Diese Antwort ist nicht richtig, da ich die Tatsache fehlte, dass volatile Garantien, die volatile Variablen zugreift sind nicht neu geordnet, bietet aber keine solche Garantien in Bezug auf andere nicht volatile Zugriffe und Manipulationen. Ein Speicherzaun bietet solche Garantien und ist für diese Anwendung notwendig. Meine ursprüngliche Antwort ist unten, aber handle nicht darauf. Siehe this answer für eine gute Erklärung in dem Loch in meinem Verständnis, das zu der folgenden falschen Antwort führte.

Ursprüngliche Antwort:

Ja, Sie müssen volatile auf Ihrer rt_data Erklärung; Wenn eine Variable außerhalb des Steuerungsflusses eines zugreifenden Threads geändert werden kann, sollte sie volatile lauten. Während Sie möglicherweise ohne volatile wegkommen, da Sie in einen lokalen Zeiger kopieren, hilft volatile mindestens mit Dokumentation und verhindert auch einige Compiler-Optimierungen, die Probleme verursachen können. Betrachten Sie das folgende Beispiel, angenommen von DDJ:

volatile int a; 
int b; 
a = 1; 
b = a; 

Wenn es möglich ist, für a seinen Wert zwischen a=1 und b=a geändert haben, dann sollten avolatile deklariert werden (es sei denn natürlich, die Zuweisung eines Out-of Datumswert zu b ist akzeptabel). Multithreading, insbesondere mit atomaren Primitiven, stellt eine solche Situation dar. Die Situation wird auch mit Variablen ausgelöst, die durch Signalbehandlungsroutinen und durch Variablen modifiziert sind, die auf ungerade Speicherstellen (z. B. Hardware-E/A-Register) abgebildet sind. Siehe auch this question.

Sonst sieht es gut für mich aus.

In C würde ich wahrscheinlich die atomaren Primitiven von GLib dafür verwenden. Sie verwenden eine atomare Operation, sofern verfügbar, und greifen auf eine langsam aber korrekte Mutex-basierte Implementierung zurück, wenn die atomaren Operationen nicht verfügbar sind. Boost kann etwas ähnliches für C++ bereitstellen.

+5

Flüchtigen hat nichts mit Gleichzeitigkeit zu tun, sie sind völlig orthogonal. Beide befassen sich mit der Erzwingung von Laden/Speichern und Neuordnen, aber die von volatilen bereitgestellten Garantien lösen keine gleichzeitigen Probleme. –

+0

@Caspin Ja, flüchtig ist orthogonal zu Gleichzeitigkeitsproblemen und volatile allein reicht nicht aus. Es ist jedoch mein Verständnis, dass volatile in der gleichzeitigen Programmierung nützlich ist, um sicherzustellen, dass Threads die Änderungen des jeweils anderen sehen. Es gibt keinen großen Unterschied zwischen einer Variablen, die von einem anderen Thread geändert wird, und einer Änderung durch einen Hardware-Interrupt - beide verletzen die Annahmen, die für das Laden/Speichern von Neuordnung erforderlich sind, und volatile sagt dem Compiler, dass diese Annahmen nicht unbedingt gelten. –

+0

Orthogonal bedeutet, dass Probleme flüchtige Lösungen nicht die Art von Problemen sind, die durch gleichzeitige Programmierung entstehen. Insbesondere gilt die Bestellgarantie von volatile nur für den aktuellen Thread und nur für andere flüchtige Produkte. Bitte lesen Sie die Links in meiner Antwort, da die gesamte Beschreibung von volatile zu komplex ist, um in einen Kommentar zu passen. Oder besser noch stackoverflow fragen "Warum ist volatile nicht nützlich für die gleichzeitige Programmierung mit c/C++?" und ich werde eine eingehende Antwort geben. –