Datenkonsistenz in Multithreadedanwendungen
-
Hi,
ich habe seit ein paar Tagen eine Frage auf die ich keine Antwort gefunden habe.
Bei c/C++ multithreaded Anwendungen kann bzw. sollte man ja auf das volatile keyword verzichten. Wer sorgt dann allerdings dafür, dass die Daten weiterhin synchronisiert sind (in einer critical section)
weil der Compiler ja trotzdem wild optimieren kann.erkennt das dann das Betriebssystem oder macht das die CPU? oder ganz anders?
-
Man sollte nicht explizit auf
volatile
verzichten, viel mehr hatvolatile
in C++ mit Multithreading und speziell Synchronisierung nicht viel zu tun. Und die Implementierung der Critical Section sorgt dafür, dass sich jeweils nur ein Thread darin befinden kann (das ist dann meistens über OS-Funktionen oder Assembler o.ä. geregelt).
-
Stefans schrieb:
Bei c/C++ multithreaded Anwendungen kann bzw. sollte man ja auf das volatile keyword verzichten. Wer sorgt dann allerdings dafür, dass die Daten weiterhin synchronisiert sind (in einer critical section)
weil der Compiler ja trotzdem wild optimieren kann.Dafür sorgt wie immer schon der Programmierer indem er sich entsprechender Synchronisationsprimitive wie z.B. CriticalSections, Mutexe, irgendwelche Atomic-Operations, ... bedient.
volatile
hat dafür jedenfalls noch nie gesorgt und der Compiler schon gar nicht und die CPU sowieso nicht.
-
Ok, anderst:
Man sollte das volatile Keyword in einer synchronisierten Multithreaded Umgebung nicht verwenden, da der Compiler ansonsten vom Optimieren abgehalten wird.Zu meiner Frage ein Psuedobeispiel:
//Globale Variable int a; CriticalSection c; void thread1() { while(true) { c.Enter(); if (a == 1) { c.Leave(); return; } Sleep(100); c.Leave(); } } void thread2() { Sleep(5000); c.Enter(); a = 1; c.Leave(); } void main() { a = 0; BeginThread(thread1); BeginThread(thread2); //hier noch auf threads warten }
Der Compiler könnte den Code für Thread1 so optimieren, dass dort die Variable a in einem Register gehalten wird. In einer Betriebssystemlosen Embedded Firmware könnte sich dadurch Thread1 z.B. nicht beenden, da er ja die lokale Änderung von a nicht mitbekommt.
Bei Betriebssystem wird jedoch garantiert, dass das obige Beispiel funktioniert, sofern es korrekt synchronisiert ist. Nur wer sorgt dafür, dass das so ist?
Der Compiler ist es wohl nicht. Regelt die Aktualisierung dann das Betriebssystem?
-
dot schrieb:
Dafür sorgt wie immer schon der Programmierer indem er sich entsprechender Synchronisationsprimitive wie z.B. CriticalSections, Mutexe, irgendwelche Atomic-Operations, ... bedient.
volatile
hat dafür jedenfalls noch nie gesorgt und der Compiler schon gar nicht und die CPU sowieso nicht.Ja schon klar, dass der Programmierer das macht. Nur wie wird das technisch realisiert? Das ganze läuft ja völlig transparent für den Entwickler ab, also muss es ja einen speziellen Mechanismus geben, der eventuelle Registerinhalte flusht updated oder sonstwas mit macht.
-
Achso das meinst du. Naja, da da ja irgendwelche Funktionen aufgerufen werden deren Code er nicht kennt, würde ich mal vermuten der Compiler kann sich da nicht sicher sein ob nicht irgendwelche Seiteneffekte den Wert von a verändern. Wobei ich mir nicht ganz sicher bin ob der Code da ohne volatile nicht vielleicht doch undefiniertes Verhalten wär.
-
Ich verstehe die Frage nicht ganz, möchte aber auf ein paar Dinge hinweisen: Es gibt Sequenzpunkte. Der Compiler kann nur anhand der as-if Regel Instruktionen umsortieren. Und wenn irgendwo ein Funktionsaufruf dabei ist, der ja wer weiß was für Seiteneffekte haben kann, dann wird da auch nichts umsortiert. Mit anderen Worten: Solange du kein undefiniertes Verhalten hervorrufst, passiert Dir auch nichts. Der aktuelle C++ Standard behandelt Multithreading leider gar nicht. Das wird mit dem kommenden Standard nachgeholt. Darin wird ein sogenanntes "memory modell" definiert, was u.a. auch eine Definition von "data race" enthält. Wenn man keine "data races" haben will, muss man dann atomics oder mutexes und locks verwenden.
-
krümelkacker schrieb:
Ich verstehe die Frage nicht ganz, möchte aber auf ein paar Dinge hinweisen: Es gibt Sequenzpunkte. Der Compiler kann nur anhand der as-if Regel Instruktionen umsortieren. Und wenn irgendwo ein Funktionsaufruf dabei ist, der ja wer weiß was für Seiteneffekte haben kann, dann wird da auch nichts umsortiert.
Da würde ich nciht meine Hand drauf verwetten. Mit Inlinig der Seiteneffekte kann da durchaus kräftig rumsortiert werden, auf jeden Fall so, dass die "as if" Regel nicht verletzt wird, aber ein anderer Thread dennoch seltsame Zwischenwerte sehen kann.
Wen das Thema interessiert, Herb Sutter hat einiges dazu zu sagen:
http://herbsutter.com/2010/09/24/effective-concurrency-know-when-to-use-an-active-object-instead-of-a-mutex/
und alle Links dort.
-
dot schrieb:
Wobei ich mir nicht ganz sicher bin ob der Code da ohne volatile nicht vielleicht doch undefiniertes Verhalten wär.
Man benötigt definitiv kein volatile. Das weiß ich a) aus eigener Erfahrung, da ich da ansonsten schon in Probleme gelaufen wäre und b) gibt es dazu einige Quellen im Internet, die das Gleiche aussagen.
Ich formuliere meine Frage noch einmal um:
Im oben genannten Beispiel könnte der Compiler bei der thread1 Funktion auf die Idee kommen a in einem Register zu halten. Dann würde thread1 in einer Endlosschleife laufen, da er ja die Änderung von a in thread2 dann nicht mitbekommen würde. Irgendein Mechanismus muss nun also existieren, damit o.g. Beispiel funktioniert. Ich wüsste gerne welcher. Erkennt der Compiler, dass es sich hierbei um eine Critical Section handelt und liest deshalb Variable a immer neu aus dem Speicher oder geschieht das durch einen anderen Mechanismus?
-
StefanS schrieb:
dot schrieb:
Wobei ich mir nicht ganz sicher bin ob der Code da ohne volatile nicht vielleicht doch undefiniertes Verhalten wär.
Man benötigt definitiv kein volatile. Das weiß ich a) aus eigener Erfahrung, da ich da ansonsten schon in Probleme gelaufen wäre
Ein char hat immer genau acht Bit. Das weiß ich aus eigener Erfahrung, denn bisher wars noch immer so.
-
StefanS schrieb:
Man benötigt definitiv kein volatile. Das weiß ich a) aus eigener Erfahrung, da ich da ansonsten schon in Probleme gelaufen wäre und b) gibt es dazu einige Quellen im Internet, die das Gleiche aussagen.
Das heißt noch lange nicht dass der C++-Standard garantiert dass das immer so funktioniert...
-
Michael E. schrieb:
Ein char hat immer genau acht Bit. Das weiß ich aus eigener Erfahrung, denn bisher wars noch immer so.
Quelle: http://drdobbs.com/cpp/184403766
Inside a critical section defined by a mutex, only one thread has access. Consequently, inside a critical section, the executing code has single-threaded semantics. The controlled variable is not volatile anymore — you can remove the volatile qualifier.
Autor ist übrigens Andrei Alexandrescu
dot schrieb:
StefanS schrieb:
Man benötigt definitiv kein volatile. Das weiß ich a) aus eigener Erfahrung, da ich da ansonsten schon in Probleme gelaufen wäre und b) gibt es dazu einige Quellen im Internet, die das Gleiche aussagen.
Das heißt noch lange nicht dass der C++-Standard garantiert dass das immer so funktioniert...
C++ kennt von Natur aus keine Threads. Aber wie gesagt siehe Quelle oben
-
StefanS schrieb:
Michael E. schrieb:
Ein char hat immer genau acht Bit. Das weiß ich aus eigener Erfahrung, denn bisher wars noch immer so.
Quelle: http://drdobbs.com/cpp/184403766
Inside a critical section defined by a mutex, only one thread has access. Consequently, inside a critical section, the executing code has single-threaded semantics. The controlled variable is not volatile anymore — you can remove the volatile qualifier.
Zum Thema hab ich gar nichts gesagt. Ich hab nur gesagt, dass Punkt 1 deiner Begründung wertlos ist.
-
Zum Thema hab ich gar nichts gesagt. Ich hab nur gesagt, dass Punkt 1 deiner Begründung wertlos ist.
Ja für Beweisfetischisten ist so eine Aussage wertlos.
-
Mit Fetischismus hat das noch nicht mal viel zu tun. Beim Standard hat man häufig undefiniertes Verhalten, ohne es durch Ausprobieren zu merken.
-
Stefans schrieb:
Bei c/C++ multithreaded Anwendungen kann bzw. sollte man ja auf das volatile keyword verzichten. Wer sorgt dann allerdings dafür, dass die Daten weiterhin synchronisiert sind (in einer critical section)
weil der Compiler ja trotzdem wild optimieren kann.Kann er eben nicht.
Im Prinzip gibt es grob zwei Arten wie garantiert wird, dass es klappt.-
Der Compiler "kennt" Multithreading, inklusive der diversen zur Synchronisierung verwendeten Funktionen (CreateThread, EnterCriticalSection, WaitForSingleObject etc.). Dann muss er auch wissen was er wo machen darf bzw. nicht machen darf. Ich kenne allerdings kein System, wo das so laufen würde. Also nur der Vollständigkeit halber.
-
Die zur Synchronisierung verwendeten Funktionen sind für den Compiler "undurchsichtig". Damit meine ich, der Compiler kann den Code dieser Funktionen nicht analysieren, bzw. weiss, dass er es verdammtnochmal nicht tun sollte.
D.h. der Compiler muss damit rechnen, dass diese Funktionen alles mögliche tun können. Das ist der Knackpunkt.
(Bei Plattformen die etwas wie DLLs oder Shared-Objects unterstützen, kann man im Normalfall davon ausgehen, dass alles was in einer DLL bzw. einem Shared-Object implementiert ist, für den Compiler "undurchsichtig" ist.)
Einfaches Beispiel:
void UndurchsichtigeFunktion(); extern int a; // global void Foo() { for (a = 0; a < 100; a++) UndurchsichtigeFunktion(); }
Der Compiler muss hier a vor jedem Aufruf von UndurchsichtigeFunktion() in den Speicher zurück schreiben, und nach jedem Aufruf neu aus dem Speicher laden. UndurchsichtigeFunktion() könnte ja z.B. so aussehen:
extern int a; void UndurchsichtigeFunktion() { printf("a = %d\n", a); if (a == 23) a = 42; }
OK, globale Variablen sind ein Thema, aber es gibt ja auch noch andere Möglichkeiten. z.B.
void Foo() { int a = 0; // lokal CreateThread(..., MyThreadFunction, &a); while (1) { Sleep(123); UndurchsichtigeFunktion1(); // macht z.B. EnterCriticalSection(irgendwasGlobales);, aber das weiss der Compiler nicht if (a == 42) break; UndurchsichtigeFunktion2(); // macht z.B. LeaveCriticalSection(irgendwasGlobales);, aber das weiss der Compiler auch nicht } UndurchsichtigeFunktion2(); }
Nun, da CreateThread auch undurchsichtig ist, muss der Compiler mit folgendem Rechnen:
int* global_a_ptr = 0; void CreateThread(... int* a ...) { global_a_ptr = a; } void UndurchsichtigeFunktion1() { (*global_a_ptr)++; } void UndurchsichtigeFunktion2() { (*global_a_ptr)++; }
----
Soweit ist also mal, über Mechanismen die mit Multithreading gar nix zu tun haben, sichergestellt, dass der Compiler nicht wild reordern/optimieren kann.
Und um den Rest, also die nötigen Memory-Fences etc., kümmern sich dann die diversen zur Synchronisierung verwendeten Funktionen selbst.
ps: kritisch könnte es werden, wenn man
a) Synchronisierung über Inline-Assembler macht und
b) der Compiler Inline-Assembler nicht als undurchsichtig behandelt (sondern analysiert) aber
c) nicht mitbekommt dass hier ein Thread-Synchronisierungs-Konstrukt verwendet wirdAFAIK muss man da z.B. bei GCC aufpassen, und spezielle "Hinweise" in den Inline-Assembler-Code reintun, damit GCC weiss was abgeht.
-
-
pumuckl schrieb:
krümelkacker schrieb:
Ich verstehe die Frage nicht ganz, möchte aber auf ein paar Dinge hinweisen: Es gibt Sequenzpunkte. Der Compiler kann nur anhand der as-if Regel Instruktionen umsortieren. Und wenn irgendwo ein Funktionsaufruf dabei ist, der ja wer weiß was für Seiteneffekte haben kann, dann wird da auch nichts umsortiert.
Da würde ich nciht meine Hand drauf verwetten. Mit Inlinig der Seiteneffekte kann da durchaus kräftig rumsortiert werden, auf jeden Fall so, dass die "as if" Regel nicht verletzt wird, aber ein anderer Thread dennoch seltsame Zwischenwerte sehen kann.
Das mit dem Inlining stimmt. Ich kann mir aber vorstellen, dass Compilerhersteller und Standardbibliotheksautoren das Locking und Unlocking der Mutexes über Funktionen bereitstellt, die garantiert nicht "geinlined" werden. Wenn Du komische Effekte siehst (seltsame Zwischenwerte) hast Du wahrscheinlich schon undefiniertes Verhalten hervorgerufen (zB wegen eines "data race"s).
Ich würde mir jedenfalls weniger Sorgen darüber machen, was ein Compiler alles so tun und umsortieren kann und mich mehr dafür interessieren, was im kommenden Standard zum Thema "data race" stehen wird. Sofern sich Compilerhersteller und Programmierer an das halten, was im kommenden Standard dazu steht ist alles i.O. Mit Atomics und Mutexes (und deren korrekte Anwendung) ist man da auf sicherer Seite.