Speicherzugriff bei Threads
-
mrnope schrieb:
Hlymur schrieb:
Können also 2 Threads auf dieselbe Varaible zugreifen(wobei nur ein Thread schreibt) ?
Nein. Zweimal lesen ist ok, aber Lesen und Schreiben (oder auch zweimal Schreiben) nicht. Das Schreiben könnte halbfertig sein, also nur 1 Byte von den 4 geschrieben, und der andere Thread liest einen inkonsistenten zustand.
Jain, theoretisch schon. Praktisch wird das aber gerade hier nicht passieren.
Zum ersten: Unsigned int, das ist eigentlich ein Datum, welches die Wortbreite des Prozessors genau füllt. Und daher ist es auch als atomare Operation möglich und wird vom Compiler/CPU auch so durchgewunken. Klar bei abweichenden Datentypen hast du Recht. Von 4 Byte jeweils eins nacheinander kopieren ist alles, aber nicht effizient.
Zweitens meinte OP, dass seine Iterationen lange dauern, die Wahrscheinlichkeit, dass genau in dem Moment gelesen wird, wo geschrieben wird, die ist verdammt gering. Theoretisch nicht ausschließbar, praktisch aber eigentlich schon.
Aber mit dem std::atomic<int> bist du auf der sicheren Seite und dir kann nichts passieren.
-
Vielen DAnk für die schnelle Antwort

std::Atomic war genau das, was ich gesucht hab
-
Skym0sh0 schrieb:
Zum ersten: Unsigned int, das ist eigentlich ein Datum, welches die Wortbreite des Prozessors genau füllt.
Welchen Prozessors denn? Für gängige Prozessoren aber freilich richtig.
Das Hauptproblem in der Praxis wird hier sein, dass der Compiler nicht sieht, dass der Wert der Variable von anderen Threads verändert werden kann und daher in einem Register zwischenspeichern kann. Würdest du während dem Schleifendurchlauf den Zähler in einem anderen Thread wieder zurücksetzen, würde das die Schleife wahrscheinlich nicht jucken (je nach Compiler, Einstellungen und was so in der Schleife passiert). Umgekehrt könnte es sein, dass dein anderer Thread immer den gleichen Zählerstand anzeigt, weil der Compiler gar nicht einsieht, warum der sich je ändern sollte. Die Gefahr besteht mit atomic<uint> nicht.Skym0sh0 schrieb:
Theoretisch nicht ausschließbar, praktisch aber eigentlich schon.
Wenn du deine parallelen Anwendungen mit der Einstellung schreibst, dass man mit 99.999999% Erfolgswahrscheinlichkeit Fehler in der Praxis ausschließen kann, wird deine Anwendung garantiert spätestens nach 5-10 Minuten abstürzen, obwohl das ja praktisch unmöglich hätte sein dürfen.
-
Was du gebaut hast läuft unter dem Stichwort "benign data race". Benign heißt harmlos, aber so harmlos ist es dann im Regelfall doch nicht.
-
Da ist gar nichts benign dran. qwas hat das schon richtig beschrieben.
-
Das Hauptproblem in der Praxis wird hier sein, dass der Compiler nicht sieht, dass der Wert der Variable von anderen Threads verändert werden kann und daher in einem Register zwischenspeichern kann. Würdest du während dem Schleifendurchlauf den Zähler in einem anderen Thread wieder zurücksetzen, würde das die Schleife wahrscheinlich nicht jucken (je nach Compiler, Einstellungen und was so in der Schleife passiert). Umgekehrt könnte es sein, dass dein anderer Thread immer den gleichen Zählerstand anzeigt, weil der Compiler gar nicht einsieht, warum der sich je ändern sollte.
Denkt man deine Behauptung weiter, dann wuerde es auch nicht helfen, die int-Variable mit einer CriticalSection oder aehnliches zu schuetzen? Da jeder einzelne Thread die Variable in einem Register zwischenspeichern kann?
ansonsten: http://herbsutter.com/2014/01/13/gotw-95-solution-thread-safety-and-synchronization/
-
knivil schrieb:
Denkt man deine Behauptung weiter, dann wuerde es auch nicht helfen, die int-Variable mit einer CriticalSection oder aehnliches zu schuetzen? Da jeder einzelne Thread die Variable in einem Register zwischenspeichern kann?
Natürlich hilft das. Solche Operationen bilden eine memory barrier, daher ist es sicher, wenn alle deine Zugriffe innerhalb deiner critical section passieren. Das wird dir zum einen von nicht-C++-Funktionen wie pthread_mutex_lock() garantiert, aber auch vom C++-Standard für mutexes allgemein.
-
Hlymur schrieb:
Heyhey @ all,
ich habe eine Frage, welche ich mir nicht auf anhieb selber beantworten konnte (d.h. ich war auch zu dumm, um bei Google etwas gescheites zufinden).
Es geht um folgendes Problem:
Ich habe 2 Threads und eine globale unsigned int Vaiable.
Im ersten Thread hab ich eine for()-Schleife, die die globale Variable als Laufvaraible verwendet und in jedem durchlauf ieine lange-komplexe Rechnung macht.
Mein zweiter Thread soll quasi den "Status" anzeigen, d.h. ich will in dem Thread auf die globale Variable zugreifen, sie Lesen un dann irgendwie anzeigen lassen. Ich hab das auch mal getestet und es lief, jetzt weiß ich nur nicht, ob dass ein netter Zufall war oder ob das grundsätzlich ok ist.
Können also 2 Threads auf dieselbe Varaible zugreifen(wobei nur ein Thread schreibt) ?Danke und mfG
Hlymurunsigned int ist ok, das brauchst du nicht schützen.
Es ist nicht so, dass der Computer byte für byte schreibt, sondern er arbeitet auf einer bestimmten Wortbreite, z.B. 32bit.
Kannst die Variable ja noch volatile machen, dann sollte der Compiler die nicht in einem Register halten.
-
@knivil
Bei einem Compiler derEnterCriticalSection/LeaveCriticalSectionnicht als Memory-Barriers behandelt UND grundsätzlich ungeeignet ist korrekten multithreaded Code zu generieren könnte es theoretisch Probleme geben.Allerdings nicht das Problem dass ne Variable im Register gehalten wird und nicht zurückgeschrieben/nicht neu gelesen wird.
Und zwar deswegen nicht, weil der Compiler den Code vonEnterCriticalSection/LeaveCriticalSectionnicht kennt - sind ja Kernel Calls.Daher muss er davon ausgehen, dass
EnterCriticalSection/LeaveCriticalSectionsämtliche Speicherstellen lesen oder schreiben könnte deren Adresse ihnen bekannte sein könnte.
D.h. sämtliche globalen Variablen müssen vor einem so einem "Aufruf ins Unbekannte" zurpckgeschrieben werden, und danach neu eingelesen.
Weiters auch sämtliche Variablen deren Adresse mal in eine globale Variable geschrieben wurde. Und auch alle deren Adresse einer anderen solchen "unbekannten" Funktion übergeben wurde.
Bzw. alles was über solche Parameter/Werte von globalen Variablen erreichbar ist.DAS Problem kann es also schonmal nicht geben.
-----
Dafür kann es andere Probleme geben. Und zwar darf ein Compiler ja die "as if" Regel ausnutzen. Und ein Compiler der keine Threads unterstützt kann da einige Optimierungen machen, die einem "as if" Compiler strengstens verboten sind.
Folgende Transformation wäre z.B. für einen "keine Threads" Compiler OK:
int a, b, c, d, someGlobal; // void Foo() { if (someGlobal < 10) { someGlobal = 123; a++; } else if (someGlobal < 50) { someGlobal = 123; b++; } else if (someGlobal != 123) { someGlobal = 123; c++; } else { d++; } assert(someGlobal == 123); } // -> void Foo() { if (someGlobal < 10) a++; else if (someGlobal < 50) b++; else if (someGlobal != 123) c++; else d++; someGlobal = 123; assert(someGlobal == 123); }Ohne Threads ist der "zusätzliche" Schreibzugriff auf
someGlobal, der passiert wenn der Code im letzten "else" Zweig landet, OK. Nachher steht sowieso immer 123 drinnen, und der Schreibzugriff als solcher ist nicht beobachtbar (die Variable ist ja nichtvolatile).Mit Threads wird hier aber ne Variable beschrieben die im Originalprogramm nicht beschrieben wurde. Und das ist nicht OK, selbst wenn man den selben Wert drüberschreibt den die Variable davor schon hatte.
Anderes, ähnliches Beispiel:
void Bar(char* someArray) { for (size_t i = 0; i < 1000; i++) if (i != 42) someArray[i] = 0; } -> void Bar(char* someArray) { char bak = someArray[42]; memset(someArray, 0, 1000); // VIEL schneller als jedes char einzeln zu setzen someArray[42] = bak; }Auch hier wieder: ohne Threads OK, gibt ja keinen der zwischenzeitliche Änderungen von non-volatile Speicherstellen beobachten könnte.
Mit Threads aber nicht OK, weil eben was geschrieben wird was das original Programm nicht schreibt.
-
fsdfsdfs schrieb:
unsigned int ist ok, das brauchst du nicht schützen.
Es ist nicht so, dass der Computer byte für byte schreibt, sondern er arbeitet auf einer bestimmten Wortbreite, z.B. 32bit.
Kannst die Variable ja noch volatile machen, dann sollte der Compiler die nicht in einem Register halten.Das ist in der Praxis auf vielen Systemen so, ja.
Aber nicht auf allen.
unsigned intkann durchaus 32 Bit sein, obwohl der Datenbus vielleicht nur 16 Bit breit ist.Und vom Standard her ist es schonmal gar nicht abgedeckt.
-
fsdfsdfs schrieb:
unsigned int ist ok, das brauchst du nicht schützen.
Ich dachte Herb Sutter & Co hätten diese Meinung ausgerottet.
Ich empfehle How to miscompile programs with "benign" data races. Da wird erklärt, warum Data Races böse sind, selbst wenn 2 Threads denselben Wert in eine Variable schreiben, selbst auf x86 mit einem int.
-
unsigned int ist ok, das brauchst du nicht schützen
Nun, ich hoffte mein Link greift dem vorweg. Hat sich aber niemand durchgelesen.
@hustbaer: Jaja, ich weiss das alles. Die Frage ist nur, warum sind Funktionsaufrufe anders im Vergleich zu normalen Variablenzugriffen? Warum nicht vor jedem Lesezugriff eine Memorybarrier, wobei moeglichst viele wegoptimiert werden, da die meisten beweisbar unnoetig sind.
Die Sache ist die:
Umgekehrt könnte es sein, dass dein anderer Thread immer den gleichen Zählerstand anzeigt
Ich habe damals (lange vor C++11) echt probiert, genau das zu erreichen. Soweit ich mich erinnern kann, hat es nicht geklappt.
Zu der Transformation: Leider kenne ich mich mit C++11 nicht in den Feinheiten aus. Habe den Standard noch nicht gelesen. Nur im letzten else-Zweig wird someGlobal nicht beschrieben, sonst immer. D.h. die Transformation ist so (vielleicht) nicht erlaubt, aber nur weil es sich an der Stelle um d++ anders verhaelt. Andere Transformationen bezueglich den ersten if-Bedingungen koennten aber erlaubt sein.
-
knivil schrieb:
@hustbaer: Jaja, ich weiss das alles.
Wenn du das alles weisst, dann hast du's offensichtlich nicht verstanden.
knivil schrieb:
Die Frage ist nur, warum sind Funktionsaufrufe anders im Vergleich zu normalen Variablenzugriffen? Warum nicht vor jedem Lesezugriff eine Memorybarrier, wobei moeglichst viele wegoptimiert werden, da die meisten beweisbar unnoetig sind.
Achje, das hab ich doch schon erklärt...
Weil eben die meisten gerade nicht "beweisbar unnötig" sind. Ausser man verbietet eben Data-Races. Was genau das ist was das der C++ Standard mit seinem Memory Model macht.
Es gibt schon so viel zu viele Stellen wo - auch mit den relativ "schwachen" Garantien die der neue C++ Standard bietet - Werte zurückgeschrieben oder neu gelesen werden müssen. Sogar ganz ohne Multithreading. Jeder Aufruf in eine DLL/SO, fast jeder
virtualAufruf oder Aufruf über Funktionszeiger. Und jeder Aufruf einer Funktion wo der Code-Generator aus welchen Gründen auch immer die aufgerufene Funktion nicht genauestens unter die Lupe nehmen kann. Was bei aktuellen Compilern vermutlich jeder Aufruf sein wird der nicht inline erweitert wird.Falls du anderer Meinung bist, dann liefere doch bitte eine exakte Definition. Also wie würde dein Memory-Model aussehen? Wenn du 'was brauchbares zusammenbringst ist die Ruhm & Ehre in der Programmierer-Community sicher.
Die Sache ist die:
Umgekehrt könnte es sein, dass dein anderer Thread immer den gleichen Zählerstand anzeigt
Ich habe damals (lange vor C++11) echt probiert, genau das zu erreichen. Soweit ich mich erinnern kann, hat es nicht geklappt.
Die allermeisten (*) Funktionen um etwas "anzuzeigen" sind welche die der Compiler nicht "sehen" kann. Daher kann erkeine Annahmen darüber treffen, und muss sie daher wie
EnterCriticalSection/LeaveCriticalSectionbehandeln.
Ist doch irgendwie logisch.Ein einfaches
while (var) othervar++;hat sogar schon VC6 zu
reg = var; while (reg) othervar++;optimiert. Das weiss ich ganz sicher, weil ich es ausprobiert habe.
Sobald du natürlich einprintf(var)in den Loop reinschreibst ist der Spuk vorbei. Weilprintfirgendwo nen Kernel-Call enthält. Und ich kenne keinen Compiler der Annahmen darüber trifft was welche Kernel-Calls tun dürfen und was nicht.*: Wobei "allermeisten" hier quasi mit "alle" gleichzusetzen ist. Klar, Funktionen die direkt in den Video-Speicher schreiben gingen vermutlich. Und mit solchen Funktionen könnte man es vermutlich sogar hinbekommen. Ist aber eigentlich vollkommen uninteressant ob man so einen Fall real konstuieren kann oder nicht.
-
hustbaer schrieb:
Ein einfaches
while (var) othervar++;hat sogar schon VC6 zu
reg = var; while (reg) othervar++;optimiert. Das weiss ich ganz sicher, weil ich es ausprobiert habe.
Sobald du natürlich einprintf(var)in den Loop reinschreibst ist der Spuk vorbei. Weilprintfirgendwo nen Kernel-Call enthält. Und ich kenne keinen Compiler der Annahmen darüber trifft was welche Kernel-Calls tun dürfen und was nicht.Ich dachte, der Spuk sei vorbei, weil der Compiler annehmen muss, daß das Register von printf zerstört wird.
-
Falls du anderer Meinung bist, dann liefere doch bitte eine exakte Definition. Also wie würde dein Memory-Model aussehen? Wenn du 'was brauchbares zusammenbringst ist die Ruhm & Ehre in der Programmierer-Community sicher.
Jetzt geht das wieder los ...
Und VC 6 als Referenz mit seiner Standardkonformitaet halte ich fuer fragwuerdig. Damals habe ich gcc 3.irgendwas benutzt, pthreads und habe mehr als nichts gemacht, die Variable muss direkt oder indirekt beobachtbar sein, sonst hats keinen Sinn. Und natuerlich kann das Programm den Inhalt der Register sichern und wiederherstellen ... Aber ich sehe wo das wieder hinfuehrt, kein Bock drauf.
-
fsdfsdfs schrieb:
unsigned int ist ok, das brauchst du nicht schützen.
Es ist nicht so, dass der Computer byte für byte schreibt, sondern er arbeitet auf einer bestimmten Wortbreite, z.B. 32bit.
Kannst die Variable ja noch volatile machen, dann sollte der Compiler die nicht in einem Register halten.Falsch. Folgendes Programm:
#include <stdio.h> #include <pthread.h> #define LOOPS 1000000 int counter = 0; void* inc(void* p) { unsigned n; for (n = 0; n < LOOPS; ++n) ++counter; } void* dec(void* p) { unsigned n; for (n = 0; n < LOOPS; ++n) --counter; } int main() { pthread_t incThread, decThread; pthread_create(&incThread, 0, inc, 0); pthread_create(&decThread, 0, dec, 0); pthread_join(incThread, 0); pthread_join(decThread, 0); printf("%d\n", counter); }Sollte eigentlich immer 0 ausgeben, da ja der eine Thread die Variable eine Million mal inkrementiert und der andere eine Million mal dekrementiert. Ich habe das Programm unter debian auf VMWare mit einem Core (x86) übersetzt und laufen gelassen. Es eignet sich aber eher als Zufallszahlengenerator.
Übrigens habe ich das mit:
gcc -o foo -lpthread foo.cübersetzt. Wird das mit -O2, also optimiert übersetzt, dann kommt immer 0 raus. Was aber nicht wirklich überraschend ist, da so eine Schleife leicht zu optimieren ist.
-
Hä?
Ja und?Was zeigt das Programm so? oder was soll es zeigen?
Und infwiefern verneint es die volatile-Aussage?So zeigt dein Programm nur, dass mit Concurrency nicht zu spaßen ist, wenn man nicht aufpasst... aber mehr auch nicht.
Und dass mit Optimierungen der Compiler die Schleife zu einem Konstanten Ausdruck zusammenfasst ist schön, aber auch nicht aussagekräftig.
-
Skym0sh0 schrieb:
Und infwiefern verneint es die volatile-Aussage?
volatile hindert den Compiler im Prinzip daran, den Wert in einem Register zu halten. Es garantiert aber nicht Atomizität; obiges Beispiel wär auch mit volatile undefiniertes Verhalten.
-
Skym0sh0 schrieb:
Hä?
Ja und?Was zeigt das Programm so? oder was soll es zeigen?
Und infwiefern verneint es die volatile-Aussage?Das habe ich bewusst zitiert:
fsdfsdfs schrieb:
unsigned int ist ok, das brauchst du nicht schützen.
Und genau diese Aussage habe ich widerlegt. Ich habe mich auf keine volatile-Aussage bezogen. Bei der Diskussion geht es doch darum, ob man eine globale Variable beim parallelen Zugriff schützen muss. Und die Antwort lautet: ja.
Im übrigen ändert sich nichts, wenn wenn ich die Variable volatile mache.
-
Also, ich hatte nie Probleme, solches Verhalten nachzubilden - das hier reicht schon:
#include <iostream> #include <thread> int main(int argc,char** argv) { std::size_t counter=0; std::thread([&]() {for(;;)counter++;}).detach(); for(;;) { std::cout << counter << std::endl; std::this_thread::sleep_for(std::chrono::milliseconds(1000)); } }Gibt bei mir mit gcc 4.8.1 und -O2 immer 0 aus.
Hab auch sonst schon oft ausprobiert, was passiert, wenn ich gemeinsam verwendete Variablen nicht angemessen schütze. Die Antwort ist oft einfach: es passiert nichts mehr, irgendein wichtiger Thread optimiert sich quasi selbst weg und hält den Betrieb auf.