2010-05-01 8 views
11

So ist mir bewusst, dass nichts in C++ atomar ist. Aber ich versuche herauszufinden, ob es "pseudo-atomare" Annahmen gibt, die ich machen kann. Der Grund ist, dass ich Mutexe in einigen einfachen Situationen vermeiden möchte, in denen ich nur sehr schwache Garantien brauche."Pseudo-atomare" Operationen in C++

1) Angenommen, ich habe global definiert flüchtige Bool b, die zunächst ich wahr gesetzt. Dann starte ich einen Thread, der eine Schleife ausführt

while(b) doSomething(); 

Inzwischen, in einem anderen Thread, führe ich b = wahr.

Kann ich davon ausgehen, dass der erste Thread weiterhin ausgeführt wird? Mit anderen Worten, wenn b als wahr beginnt und der erste Thread den Wert von b zur gleichen Zeit überprüft wie der zweite Thread b = wahr zuweist, kann ich dann davon ausgehen, dass der erste Thread den Wert von b als wahr liest? Oder ist es möglich, dass an einem Zwischenpunkt der Zuweisung b = wahr, der Wert von b könnte als falsch gelesen werden?

2) Nun nehme an, dass b anfänglich falsch ist. Der erste Thread führt dann

aus, während der zweite Thread b = true ausführt. Kann ich davon ausgehen, dass bad() nie aufgerufen wird?

3) Was ist mit einem int oder anderen eingebauten Typen: angenommen, ich habe volatile int i, was anfänglich (etwa) 7 ist, und dann ich i = 7 zuweisen. Kann ich davon ausgehen, dass der Wert von i zu irgendeinem Zeitpunkt während dieser Operation aus einem beliebigen Thread gleich 7 ist?

4) Ich habe flüchtige int i = 7, und dann führe ich i ++ aus einem Thread, und alle anderen Threads lesen nur den Wert von i. Kann ich annehmen, dass ich in keinem Thread irgendeinen Wert habe, außer für 7 oder 8?

5) Ich habe volatile int ich, von einem Thread ich führe i = 7, und von einem anderen führe ich i = 8. Hinterher, ist es garantiert entweder 7 oder 8 (oder was auch immer zwei Werte, die ich gewählt habe)?

+0

Wo Sie haben gelesen, dass "nichts in C-Atom ist ++"? Ich hatte den Eindruck, dass alle Lese- oder Schreibvorgänge eines einzelnen Bytes atomar waren. Sie könnten also den Wert eines Bool überprüfen oder ein Char zuweisen ... oder vielleicht waren es 32 Bits, die garantiert atomar waren? Wäre das dann 64 in einer 64-Bit-Architektur? – mpen

+1

@Mark: Ich denke, er meinte, dass die C++ - Sprache nicht die Atomarität von irgendetwas garantiert. Es hängt von der spezifischen Implementierung (und der CPU-Architektur) ab, was atomar ist oder nicht. – jalf

+0

@jalf: Nun ... okay, dann ist es vielleicht keine Eigenschaft von C++, aber es ist immer noch eine einigermaßen sichere Annahme. So können wir zumindest die Frage (1) mit Sicherheit beantworten. – mpen

Antwort

14

Es gibt keine Threads in Standard C++ und Threads cannot be implemented as a library.

Daher hat der Standard nichts über das Verhalten von Programmen zu sagen, die Threads verwenden. Sie müssen prüfen, welche zusätzlichen Garantien Ihre Threading-Implementierung bietet.

das gesagt ist, in Threading-Implementierungen habe ich verwendet:

(1) ja, Sie, dass irrelevante Werte annehmen können, nicht auf Variablen geschrieben. Andernfalls wird das gesamte Speichermodell aus dem Fenster gelöscht. Aber sei vorsichtig, wenn du sagst "ein anderer Thread" setzt niemals b auf falsch, das heißt überall, immer. Wenn dies der Fall ist, könnte dieser Schreibvorgang möglicherweise neu angeordnet werden, um während der Schleife ausgeführt zu werden.

(2) Nein, der Compiler kann die Zuordnungen zu b1 und b2 neu anordnen, daher ist es möglich, dass b1 am Ende wahr und b2 falsch ist. In solch einem einfachen Fall weiß ich nicht, warum es neu ordnen würde, aber in komplizierteren Fällen könnte es sehr gute Gründe geben.

[Edit: oops, als ich zur Antwort kam (2) hatte ich vergessen, dass b volatil war. Lesen von einer volatilen Variablen wird nicht neu geordnet, sorry, also ja bei einer typischen Threading-Implementierung (wenn es so etwas gibt), können Sie davon ausgehen, dass Sie nicht mit b1 wahr und b2 falsch enden werden.]

(3) wie 1.volatile hat überhaupt nichts mit Threading zu tun. Es ist jedoch in einigen Implementierungen (Windows) ziemlich aufregend und könnte tatsächlich Speicherbarrieren beinhalten.

(4) auf einer Architektur, wo int schreibt atomare ja, obwohl volatile hat nichts damit zu tun. Siehe auch ...

(5) Überprüfen Sie die Dokumente sorgfältig. Wahrscheinlich ja, und wieder flüchtig ist irrelevant, weil auf fast allen Architekturen int schreibt atomare sind. Aber wenn int schreiben ist nicht atomar, dann nein (und nein für die vorherige Frage), auch wenn es flüchtig ist, könnte man im Prinzip einen anderen Wert bekommen. Angesichts dieser Werte 7 und 8 sprechen wir jedoch über eine ziemlich seltsame Architektur für das Byte, das die relevanten Bits enthält, die in zwei Stufen geschrieben werden, aber mit anderen Werten könnte man plausibler einen partiellen Schreibvorgang bekommen.

Für ein plausibleres Beispiel nehmen Sie an, dass Sie aus irgendeinem seltsamen Grund ein 16-Bit-Int auf einer Plattform haben, wo nur 8-Bit-Schreiboperationen atomar sind. Odd, aber legal, und seit int müssen mindestens 16 Bits sein, die Sie sehen können, wie es zustande kommen könnte. Nehmen wir weiter an, dass Ihr Anfangswert ist 255. Dann rechtlich umgesetzt werden erhöhen könnte als:

  • den alten Wert
  • Schritt in einem Register
  • das signifikanteste Byte des Ergebnisses
  • schreiben schreiben lesen die niedrigstwertiges Byte des Ergebnisses.

Ein Nur-Lese-Threads, die Inkrementierung Faden zwischen den dritten und vierten Schritten, dass unterbrochen, 511. den Wert sehen könnte, wenn die Schreibvorgänge in der anderen Reihenfolge sind, könnte es 0.

Ein sehen Inkonsistente Werte können permanent zurückbleiben, wenn ein Thread 255 schreibt, ein anderer Thread gleichzeitig 256 schreibt und die Schreibvorgänge verschachtelt werden. Unmöglich auf vielen Architekturen, aber um zu wissen, dass dies nicht passieren wird, müssen Sie zumindest etwas über die Architektur wissen. Nichts im C++ - Standard verbietet dies, weil der C++ - Standard davon spricht, dass die Ausführung durch ein Signal unterbrochen wird, ansonsten aber kein Konzept der Ausführung durch einen anderen Teil des Programms unterbrochen wird und kein Konzept der gleichzeitigen Ausführung. Deshalb sind Threads nicht nur eine andere Bibliothek - das Hinzufügen von Threads ändert das C++ - Ausführungsmodell grundlegend. Es erfordert die Implementierung, Dinge anders zu machen, wie Sie schließlich herausfinden werden, wenn Sie zum Beispiel Threads unter gcc verwenden und vergessen, -pthreads anzugeben.

Das gleiche auf einer Plattform, wo ausgerichtetint schreibt atomar sind, aber nicht ausgerichteten int schreibt sind zulässig und nicht atomar passieren könnte. Zum Beispiel IIRC auf x86, unausgerichtete int Schreibvorgänge sind nicht atomar garantiert, wenn sie eine Cache-Zeilengrenze überschreiten. x86-Compiler werden eine deklarierte int Variable aus diesem Grund und anderen nicht falsch ausrichten. Aber wenn Sie Spiele mit Strukturpackung spielen, könnten Sie wahrscheinlich ein Beispiel provozieren.

Also: so ziemlich jede Implementierung gibt Ihnen die Garantien, die Sie brauchen, aber könnte es auf eine ziemlich komplizierte Art und Weise tun.

Im Allgemeinen habe ich festgestellt, dass es nicht wert ist, sich auf plattformspezifische Garantien über Speicherzugriff zu verlassen, die ich nicht vollständig verstehe, um Mutexe zu vermeiden. Verwenden Sie einen Mutex, und wenn das zu langsam ist, verwenden Sie eine qualitativ hochwertige, lockfreie Struktur (oder implementieren Sie einen Entwurf für einen), die von jemandem geschrieben wurde, der die Architektur und den Compiler wirklich kennt.Es wird wahrscheinlich richtig sein, und vorbehaltlich der Korrektheit wird wahrscheinlich alles übertreffen, was ich selbst erfinde.

+1

§1.9 des Standards diskutiert die abstrakte Maschine, die wie ein Thread der Ausführung ist. – Potatoswatter

+0

Warum sollten Sie jeden Thread als auf einer separaten abstrakten C++ - Maschine ausgeführt betrachten? Beschreibt Ihre Threading-Implementierung, dass dies passiert? Ist das überhaupt möglich? Wenn ich threadA als separate abstrakte Maschine betrachten kann, die den Regeln von 1.9 unterliegt, dann gibt es keine legale Möglichkeit, dass eine nichtflüchtige Variable den Wert ändern kann, während ich laufe, es sei denn, ich ändere sie. Und genau das kann und passiert in allen mir bekannten Threading-Implementierungen. –

+0

@Steve: Es ist möglich, aber es ist eine schlechte Idee. Das bewirkt, dass die abstrakte Maschine, die Sie implementiert haben, gegen §1.9 verstößt. Ebenso kann ich einen Zeiger auf eine nichtflüchtige Variable an den Kernel übergeben und nach I/O dazu fragen. Das macht meine C++ - Implementierung nicht konform, es ist mein Fehler. – Potatoswatter

0

Wenn Ihre C++ - Implementierung die von n2145 angegebene Bibliothek mit atomaren Operationen oder eine Variante davon bereitstellt, können Sie sich vermutlich darauf verlassen. Ansonsten können Sie sich im Allgemeinen nicht auf "irgendetwas" über die Atomizität auf der Sprach-Ebene verlassen, da Multitasking jeglicher Art (und somit Atomizität, die sich mit Multitasking beschäftigt) vom existierenden C++ - Standard nicht spezifiziert wird.

0

Flüchtige in C++ spielt nicht die gleiche Rolle wie in Java. Alle Fälle sind undefiniertes Verhalten, wie Steve sagte. Einige Fälle können für einen Compiler, eine gegebene Prozessorarchitektur und ein Multithreading-System in Ordnung sein, aber das Umschalten der Optimierungsflags kann dazu führen, dass sich Ihr Programm anders verhält, da die C++ 03-Compiler nichts über Threads wissen.

C++ 0x definiert die Regeln, die Race Conditions und die Operationen vermeiden, die Ihnen helfen, das zu beherrschen, aber vielleicht gibt es noch keinen Compiler, der noch den ganzen Teil des Standards in Bezug auf dieses Thema implementiert.

1

Es ist in der Regel eine wirklich, wirklich schlechte Idee, sich darauf zu verlassen, da Sie am Ende mit schlechten Dingen und nur mit ein paar Architekturen enden können. Die beste Lösung wäre eine garantierte atomare API, zum Beispiel die Windows Interlocked API.

6

Die meisten Antworten adressieren die CPU-Speicherordnungsprobleme, die Sie erleben werden, aber keiner hat detailliert beschrieben, wie der Compiler Ihre Absichten vereiteln kann, indem er Ihren Code auf eine Weise neu sortiert, die Ihre Annahmen bricht.

ein Beispiel aus this post genommen Bedenken Sie:

volatile int ready;  
int message[100];  

void foo(int i) 
{  
    message[i/10] = 42;  
    ready = 1;  
} 

Bei -O2 und über die jüngsten Versionen von GCC und Intel C/C++ (weiß nicht, über VC++) wird in den Laden zu ready zuerst tun, so dass es kann mit Berechnung von i/10 überlappt werden (volatile nicht Sie speichern!):

leaq _message(%rip), %rax 
    movl $1, _ready(%rip)  ; <-- whoa Nelly! 
    movq %rsp, %rbp 
    sarl $2, %edx 
    subl %edi, %edx 
    movslq %edx,%rdx 
    movl $42, (%rax,%rdx,4) 

Dies ist kein Fehler, dann ist es der Optimierer CPU Pipelining zu nutzen. Wenn ein anderer Thread auf ready wartet, bevor Sie auf den Inhalt von message zugreifen, dann haben Sie ein böses und obskures Rennen.

Verwenden Sie Compiler Barrieren, um sicherzustellen, dass Ihre Absicht eingehalten wird. Ein Beispiel, das auch die relativ starke Ordnung der x86 nutzt ist die Freisetzung/Wrapper in Dmitriy Vyukov Single-Produzenten Einzel-Consumer-Warteschlange posted here gefunden verbrauchen:

// load with 'consume' (data-dependent) memory ordering 
// NOTE: x86 specific, other platforms may need additional memory barriers 
template<typename T> 
T load_consume(T const* addr) 
{ 
    T v = *const_cast<T const volatile*>(addr); 
    __asm__ __volatile__ ("" ::: "memory"); // compiler barrier 
    return v; 
} 

// store with 'release' memory ordering 
// NOTE: x86 specific, other platforms may need additional memory barriers 
template<typename T> 
void store_release(T* addr, T v) 
{ 
    __asm__ __volatile__ ("" ::: "memory"); // compiler barrier 
    *const_cast<T volatile*>(addr) = v; 
} 

Ich schlage vor, dass, wenn Sie in das Reich wagen, werden von Gleichzeitiger Speicherzugriff verwenden Sie eine Bibliothek, die sich um diese Details kümmert. Während wir alle auf n2145 und std::atomic warten, sehen Sie sich die tbb::atomic oder die kommenden boost::atomic von Thread Building Blocks an.

Neben Korrektheit, diese Bibliotheken Ihren Code vereinfachen und klären Sie Ihre Absicht:

// thread 1 
std::atomic<int> foo; // or tbb::atomic, boost::atomic, etc 
foo.store(1, std::memory_order_release); 

// thread 2 
int tmp = foo.load(std::memory_order_acquire); 

explizite Gedächtnis Bestellung benutzen, foo ‚s inter-thread Beziehung ist klar.

2

Vielleicht ist dieser Thread alt, aber der C++ 11-Standard hat eine Thread-Bibliothek und auch eine große atomare Bibliothek für atomare Operationen. Der Zweck ist speziell für die Nebenläufigkeitsunterstützung und vermeidet Datenrennen. Der entsprechende Header ist Atom

+0

Sie können 'std :: atomic ' anstelle von 'int' verwenden, um es atomar zu machen. Siehe http://en.cppreference.com/w/cpp/atomic/atomic. – JojOatXGME

0

Meine Antwort frustrierend sein wird: Nein, Nein, Nein, Nein, und Nr

1-4) Der Compiler erlaubt ist, alles zu tun, es mit einem gefällt Variable, in die geschrieben wird. Es kann temporäre Werte darin speichern, solange am Ende etwas geschieht, das dasselbe tun würde wie dieser Thread, der in einem Vakuum ausgeführt wird. Alles ist gültig

5) Nein, keine Garantie. Wenn eine Variable nicht atomar ist und Sie in einem Thread darauf schreiben und auf einem anderen Thread lesen oder schreiben, handelt es sich um einen Rennfall. Die Spezifikation erklärt solche Race Cases zu undefiniertem Verhalten, und absolut alles geht. Davon abgesehen werden Sie sich schwer tun, einen Compiler zu finden, der Ihnen nicht 7 oder 8 gibt, aber es ist legal für einen Compiler, Ihnen etwas anderes zu geben.

Ich beziehe mich immer auf diese sehr komische Erklärung der Rassenfälle.

http://software.intel.com/en-us/blogs/2013/01/06/benign-data-races-what-could-possibly-go-wrong