gastgast schrieb:
Ich hab doch schon geschrieben: er verwendet ne Mutex, und deswegen ist es kein Problem.
Ich habe Szenario in Verbindung mit dem memory modell in C++11 so verstanden:
Es ist solange es nur um den boolean stopped geht, kein Problem.
Korrekt.
Soll aber jetzt zusätzlich ein '\n' irgendwie zurück nach cin (über die Sinnhaftigkeit wurde ja in anderen Antworten schon diskutiert), so führt das entweder zu einem Deadlock, wenn es mit einem lock passiert, wie PI schon geschrieben hat, oder, wenn das in einem dritten Thread passiert, zu einem Data Race, weil:
Hat das Schreiben des '\n' aus einem dritten Thread heraus kein eigenes Lock, wird es entweder 'irgendwann' aus dem Cache in den Hauptspeicher geschrieben, oder wenn für den Core, auf dem der Thread läuft eine entsprechende memory fence kommt.
Ja, ohne Lock gibt es beim Modifizieren von cin ein Problem.
Ich verstehe nur nicht wie du hier auf Memory-Ordering kommst, das macht noch ganz andere, grundlegendere Probleme.
Der Sinn war ja, dass std::getline() "zurückkommen" soll. Sobald das passiert ist, ist der Rest ja ein Klacks.
Bloss C++ Streams sind nicht threadsafe, putback() trotzdem concurrent aufzurufen wäre demnach UB, und damit ist es schon vorbei.
Allein schon die Annahme dass putback() das gleichzeitig laufende std::getline() dazu bringen könnte mit Warten aufzuhören ist daher vollkommen Unsinnig und aus der Luft gegriffen.
Wenn man sich jetzt trotzdem den Kopf darüber zerbrechen möchte "warum" es denn nicht geht, oder ob es nicht vielleicht doch klappen könnte etc...
Was wird wohl der Grund sein dass std::getline() blockiert? Ganz einfach: es (bzw. der Stream) versucht mittels irgendeines OS API Calls von irgendwo weitere Daten "nachzuladen". Wenn du im Debugger auf Pause drückst wirst du vermutlich irgendwo in ReadFile, select oder was ähnlichem stehen.
Und was wird putback() machen? Nun, putback() wird irgendwie ein Zeichen "zurück" in den Lesepuffer tun. Was es ganz ganz sicher nicht machen wird, ist das Zeichen mittels einer OS API Funktion in einen Puffer des OS zurücktun.
D.h. was würde passieren? putback() würde lustig irgendwelche Datenstrukturen des Streams modifizieren, während std::readline() in ReadFile/select/... blockiert ist. Wird der ReadFile/select/... Aufruf deswegen zurückkommen? Sicher nicht, das OS schert sich nicht darum dass da eine Funktion von Klasse X was in eine Membervariable schreibt.
Aber nochmal: die ganze Überlegung ist IMO sinnlos, da wir sowieso schon längst festgestellt haben dass es UB ist.
Wenn ich die C++11 atomics richtig verstanden habe, kann ich damit das Lesen aus und Schreiben in den Hauptspeicher ohne memory fence und kompletten cache flush erzwingen. Ist das richtig?
Naja, so wie du es geschrieben hast: nein. (* siehe unten)
Ansonsten: vielleicht
Du kannst die C++0x Atomic-Operations mit "relaxed" Ordering verwenden, um einzelne Werte "atomar" zu modifizieren, ohne dabei einen vollständigen acquire/release/ordered Fence zu erzwingen.
Wenn du z.B. eine counter.Increment() Funktion mit atomic_fetch_add_relaxed(&m_counter, 1) implementierst, dann ist sichergestellt, dass kein Increment() Aufruf "verlorengeht". Du kannst den Zähler also in zig verschiedenen Threads gleichzeitig hochzählen, und am Ende werden alle Aufrufe erfasst sein.
Das Problem ist nur das "am Ende". Das setzt nämlich eine zeitliche Reihenfolge voraus, und die bekommst du mit "relaxed" eben nicht.
Das ist jetzt sehr abstrakt, daher ein einfaches Beispiel:
Counter c1; // fängt bei 0 an
Counter c2; // fängt bei 0 an
void DoSomething()
{
c1.Increment(); // verwendet atomic_fetch_add_relaxed()
blubb(); // enthält keinerlei explizite oder implizite Fences
c2.Increment(); // verwendet atomic_fetch_add_relaxed()
}
void ThreadFunction()
{
while (ShouldIContinue())
DoSomething();
}
int main()
{
std::thread t1(ThreadFunction);
std::thread t2(ThreadFunction);
while (...)
{
long val1 = c1.Get(); // verwendet atomic_load_relaxed()
long val2 = c2.Get(); // --""--
std::cout << val1 << ", " << val2 << "\n";
}
t1.join();
t2.join(); // TADAAA, *jetzt* haben wir ein definiertes davor/danach
long val1 = c1.Get(); // verwendet atomic_load_relaxed()
long val2 = c2.Get(); // --""--
std::cout "Final counts: " << val1 << ", " << val2 << "\n";
}
Guck dir das Beispiel an, und überleg dir was für Werte ausgegeben werden dürfen.
Viele würden jetzt annehmen, dass der Wert "val1" immer grösser oder gleich "val2" sein muss. c1 wird ja vor c2 hochgezählt.
Genau das ist aber nicht so, eine Ausgabe von "3, 42" wäre vollkommen OK, da eben keine "order" erzwungen wird. Der Compiler darf c1.Increment() und c2.Increment() reordern wie er mag, darf das while (ShouldIContinue()) "unrollen" und feste weiter Reordern, ganz wie er mag.
Erst nachdem die beiden Threads gejoined wurden gibt es eine (teilweise) definierte Reihenfolge. Die Ausgabe der "final counts" muss also 2x den selben Wert enthalten.
Das ist auch nur EIN Beispiel was schief gehen kann. In Wirklichkeit dürfen noch viel komischer anmutende Dinge passieren. Der Compiler darf z.B. nicht nur "relaxed" Atomic Calls reordern, er darf sie auch ganz wegoptimieren. z.B. zwei "stores" auf die gleiche Variable zusammenfassen. Oder mal "spekulativ" ein bedingtes "store" ausführen, und im Falle des "doch-nicht-Zutreffen" einer Bedingung den alten Wert wiederherstellen.
Man kann also mit "relaxed" bestimmte Dinge machen ohne Fences zu erzwingen. Nur man muss dabei verdammt aufpassen, denn viele Dinge verhalten sich dann nicht mehr so wie man es gerne hätte bzw. wie man es vielleicht erwarten würde.
Kurz: Finger weg von "relaxed". Ausgenommen diverse einfache Patterns wie "load relaxed + compute new value + compare-and-swap acquire/release/ordered".
----
*:
Sogar explizite Memory-Fences erzwingen keine Zugriffe auf den Hauptspeicher. Das System muss nur die geforderte Speichersichtbarkeit garantieren. Am einfachsten vorstellen kann man es sich vermutlich über Cache-Flushes, aber passieren tun diese nicht, bzw. nicht vollständig. Wäre auch viel zu teuer. Die CPUs/Cores in einem System machen sich das normalerweise über diverse Cache-Protokolle aus. x86/AMD64 Systeme verwenden z.B. gerne MESI bzw. Derivate davon (MOESI etc.).
Manchmal muss bzw. will man natürlich wirklich das Flushen in den Hauptspeicher verwenden. z.B. weil man das RAM testen möchte, oder auf manchen Systemen mit externem Speicher-Controller wenn man Daten von/zu Peripheriegerät übertragen muss. Für diese Fälle bieten die verschiedenen Betriebssysteme spezielle Befehle an, denen man üblicherweise auch den Adressbereich mitgibt der "geflusht" werden soll. Das muss einen durchschnittlichen C++ Programmierer aber nicht wirklich kümmern.