Threadproblem - Keine Daten an Stream -> Thread wartet



  • @gast
    Er schützt sein "stop" falg über ne Mutex, damit hat er automatisch acquire/release, d.h. es braucht keine Atomics mehr.

    ----

    Wie wenig kann man eigentlich von Multithreading verstehen, und immer noch der Meinung sein man hätte es verstanden?

    ------------------------

    @314159265358979:
    Wenn du erstmal dein Hirn einschalten würdest, bevor du Fragen wie diese hier postest, die du dir jederzeit leicht selbst hättest beantworten können, würden die Leute deine Threads vielleicht weniger "zuspammen".
    Freund der Blasmusik, werd mal etwas selbständiger.



  • Könntest du bitte konkret auf den Punkt bringen, was ich deiner Meinung nach jetzt tun soll? Deine Aussage "werd mal etwas selbstständiger" bringt mich nicht wirklich weiter.
    Weiters habe ich nirgens behauptet, Multithreading verstanden zu haben. Deshalb übe ich es, _damit_ ich das Zeug besser verstehe.



  • @314159265358979:
    Oha, der "wie wenig ..." Satz war an "gast" gerichtet, nicht an dich!

    Ansonsten...
    Wie, was du machen solltest?

    Du wolltest wissen ob es OK ist ... naja lies dein Kopfposting selbst nochmal.

    Naja... sind Operationen auf Standard-Klassen ala iostream threadsafe?
    Würdest du bei dem was du machen willst Funktionen aus zwei Threads gleichzeitig aufrufen?
    Also, kann es dann OK sein was du machen willst?

    Bzw. falls du die Antworten nicht weisst, hättest du die Fragen dazu wenigstens mal selbst aufstellen können. Und dann googeln. Falls es dich genug interessiert deine eigene Zeit dafür zu opfern meine ich natürlich.



  • hustbaer schrieb:

    Oha, der "wie wenig ..." Satz war an "gast" gerichtet, nicht an dich!

    Das habe ich missverstanden, ich habe deine Striche ---- als Trennlinie zwischen dem was du zu gast und dem was du zu mir sagst interpretiert.

    hustbaer schrieb:

    Naja... sind Operationen auf Standard-Klassen ala iostream threadsafe?

    Eigentlich nicht, nein.

    hustbaer schrieb:

    Würdest du bei dem was du machen willst Funktionen aus zwei Threads gleichzeitig aufrufen?

    Ja, deswegen war ich mir ja unsicher.

    hustbaer schrieb:

    Also, kann es dann OK sein was du machen willst?

    Da ein Thread hier wartet, war ich mir eben nicht sicher. Ich könnte mir gut vorstellen, dass eine Implementierung sowas erlaubt.

    Ich wüsste ehrlich gesagt nicht, wonach ich googeln soll. Und ja, es interessiert mich genug, um es selbst zu tun 😉



  • Also iostreams nicht threadsafe, du willst aber aus zwei Threads verwenden -> ganz klares "nicht OK".

    Da ein Thread hier wartet, war ich mir eben nicht sicher. Ich könnte mir gut vorstellen, dass eine Implementierung sowas erlaubt.

    Mal ganz abgesehen davon, dass es eh keinen Unterschied machen würde... wer sagt denn dass der wartet?
    Vielleicht ist ja just in dem Moment echter User-Input dahergekommen, und der Stream ist gerade dabei seine Eingeweide zu sortieren für den nächsten Aufruf.

    Und ob eine Implementierung es erlaubt oder nicht, ist vollkommen egal.

    Ich wüsste ehrlich gesagt nicht, wonach ich googeln soll.

    Das war für den Fall gemeint, dass du nicht weisst welche iostream Funktionen in welcher weise thread-safe sind oder eben nicht.



  • Vielleicht hilft ja das weiter.



  • @hustbaer: Ja, eigentlich ist es logisch. Danke.

    TyRoXx schrieb:

    Vielleicht hilft ja das weiter.

    An sowas habe ich auch schon gedacht, in meinem Fall mit select(). Allerdings wäre mir ein portabler weg lieber. Wenn nicht, dann werde ich es so machen, danke 🙂



  • @hustbaer: Ich wäre Dir dankbar, wenn Du statt zu polemisieren, mich auf meine Fehler hinweisen könntest - und die richtige Version lieferst. Ich lerne gerne dazu. Ironie spare ich mir jetzt...



  • Nachtrag @hustbaer:

    Wie geschrieben:

    2. Wann genau der Thread die Änderung des booleans mitbekommt ist m.W. nicht exakt definiert, da beide Threads auf separaten CPUs (oder cores einer CPU) laufen können. Wann die Variable aus dem Cache in den Hauptspeicher zurück geschrieben wird, hängt von der CPU ab. Bei x86 ist das m.W. in der Reihenfolge der Adressen, was bedeutet, dass der boolean vor dem \n in den Hauptspeicher zurück geschrieben werden kann, so dass der aus cin lesende Thread die Stop Bedingung erkennt, ohne dass noch ein \n bereit steht. Das wäre dann ein Deadlock in Folge eines Races.

    Wo ist denn da der Fehler?



  • gastgast schrieb:

    Nachtrag @hustbaer:

    Wie geschrieben:

    2. Wann genau der Thread die Änderung des booleans mitbekommt ist m.W. nicht exakt definiert, da beide Threads auf separaten CPUs (oder cores einer CPU) laufen können. Wann die Variable aus dem Cache in den Hauptspeicher zurück geschrieben wird, hängt von der CPU ab. Bei x86 ist das m.W. in der Reihenfolge der Adressen, was bedeutet, dass der boolean vor dem \n in den Hauptspeicher zurück geschrieben werden kann, so dass der aus cin lesende Thread die Stop Bedingung erkennt, ohne dass noch ein \n bereit steht. Das wäre dann ein Deadlock in Folge eines Races.

    Wo ist denn da der Fehler?

    Ja wie, wo ist der Fehler? Es stimmt halt einfach nicht. Ich hab doch schon geschrieben: er verwendet ne Mutex, und deswegen ist es kein Problem.

    Das lock() auf ne Mutex macht ein "acquire", und das unlock() ein "release".

    "acquire" und "release" sind sog. Memory-Fences, und garantieren als solche eine bestimmte "Sichtbarkeit" von Änderungen.

    Grob vereinfacht kannst du dir denken, "acquire" heisst "Read Cache flushen" und "release" heisst "Write Cache flushen".
    Oder, anderer Vergleich: "acquire" heisst "svn update" und "release" heisst "svn commit".
    (Beide Vergleiche hinken vermutlich stark, aber für einfache Fälle sind sie vielleicht ne akzeptable Annäherung)

    Und was du über x86 und "Reihenfolge der Adressen" schreibst ist auch nicht richtig (was soll "Reihenfolge der Adressen" überhaupt heissen?).
    x86 und AMD64 haben "total-store order". Das einzige was x86/AMD64 machen, ist hin und wieder mal ein "load" hinter ein "store" zu schieben. Was im Allgemeinen vollkommen unproblematisch ist.
    Es gibt einige sog. "x86" CPUs die nicht nicht "total-store order" machen, aber alles was in den letzten 5 Jahren von Intel oder AMD produziert wurde sollte OK sein.
    (Bei x86 gilt das nur für Datenzugriffe, bei AMD64 auch für das Lesen von Instructions)

    Davon abgesehen ist das relativ egal, so lange man kein Compiler-Reordering verhindert. Aber auch das übernehmen die in mutex_lock() und mutex_unlock() wohnenden Memory-Fences, d.h. auch das Compiler-Reordering ist kein Problem.

    Und das alles völlig unabhängig von der Plattform.

    (Einzige Voraussetzung ist, der verwendete Compiler muss grundsätzlich darauf ausgelegt sein "shared memory multithreading" zu ermöglichen.)

    BTW: ein paar nette Papers die u.A. über solcherlei Dinge sprechen sind hier aufgelistet:
    http://stackoverflow.com/questions/2001913/c0x-memory-model-and-speculative-loads-stores
    (Die 5 Links in der Frage selbst)

    So. Und nu muss ich nochwas sagen. Ich finde es verwunderlich, wenn Leute eine Falschinformation schreiben, man sie dann darauf hinweist (einen zugegebenermassen knappen Hinweis darauf was falsch war bzw. warum), und sie sich dann (mehr oder weniger) erbosst und/oder erstaunt zeigen, dass man ihnen nicht gleich die nötigen Grundlagen mit erklärt, die sie bräuchten, um den Hinweis verstehen zu können.



  • Ich gebe zu: Ich habe das, was ich dachte, zu kurz und unglücklich formuliert.

    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. 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.

    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?



  • 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.


Anmelden zum Antworten