threads



  • Hi 🙂

    Ich glaube, ich habe einen Fehler im Buch "Grundkurs C++" von Galileo Computing gefunden.

    Im Kapitel 12 geht es um Threads und Synchronisation. Dazu gibt es folgendes Programm:

    // listings/012/threads02.cpp
    #include <iostream>
    #include <thread>
    #include <mutex>
    #include <future>
    using namespace std;
    
    static unsigned int cnt;
    mutex mtx;
    
    void inkrement() {
      lock_guard<mutex> waechter{mtx}; // <------------------------
      ++cnt;
    }
    
    int running() {
      for(int i=0; i < 100000; ++i) 
        inkrement();
      cout << cnt << endl;
      return cnt;
    }
    
    int main( void ) {
      thread th01(running);
      thread th02(running);
      auto th03 = async( running ); 
      th01.join();
      th02.join();
      cout << th03.get() << endl;
    }
    

    Beim ersten Durchlesen ist mir die Zeile 12 seltsam vorgekommen. Eigentlich hätte ich den Mutex am Anfang der running-Funktion erwartet.

    Dann habe ich dieses Programm mal genommen und laufen lassen. Lt. Buch sollen ausgegeben werden:

    100000
    200000
    300000
    300000 (aufgrund der Ausgabe in der letzten Zeile)

    Allerdings bekomme ich:

    195326
    200000
    300000

    Daraufhin habe ich den Mutex von Zeile 12 in die running-Funktion verschoben:

    // listings/012/threads02.cpp
    #include <iostream>
    #include <thread>
    #include <mutex>
    #include <future>
    using namespace std;
    
    static unsigned int cnt;
    mutex mtx;
    
    void inkrement() {
      ++cnt;
    }
    
    int running() {
      lock_guard<mutex> waechter{mtx}; // <------------------------
      for(int i=0; i < 100000; ++i)
        inkrement();
      cout << cnt << endl;
      return cnt;
    }
    
    int main( void ) {
      thread th01(running);
      thread th02(running);
      auto th03 = async( running );
      th01.join();
      th02.join();
      cout << th03.get() << endl;
    }
    

    Jetzt scheint es richtig zu funktionieren.

    Hab ich da einen Fehler im Programm gefunden? Oder ist da jetzt irgendwas doppelt falsch und scheint nur richtig zu sein?



  • tröröö schrieb:

    Ich glaube, ich habe einen Fehler im Buch "Grundkurs C++" von Galileo Computing gefunden.

    Das ist jetzt mal was ganz neues...
    Wenn du eine Stelle gefunden hast, wo kein Fehler ist, dann kannste uns Bescheid sagen.

    Edit: Das ist sogar kein richtiger Fehler.
    Die mutex sorgt dafür, dass am Ende cnt den Wert 30000 hat.
    Deshalb lockt die vor dem Inkrement.
    Wenn du sie nach running verschiebst, sorgt sie dafür, dass der erste Thread 10000 mal inkrementiert, dann der zweite und dann der Dritte. Zufällig (vermutlich weil async auf den meisten Implementierungen sync ist) ist dies bei ... dem Autor auch so passiert.
    Wenn du aber da lockst, wo du lockst, wird im Endeffekt doch alles synchron nacheinander inkrementiert, was nicht der Sinn von Threads ist.



  • Andererseits so wie im ersten Code gelockt ist, ist es falsch, dennd as Lesen ist nicht gelockt.
    Theoretisch könnte da kompletter Bullshit ausgegeben werden.

    Aber das ist eh en schlechtes Beispiel für Synchronization...



  • Wie müsste es denn richtig ausschauen? Oder was wär denn ein korrektes, einfaches, schönes Beispiel?



  • #include <iostream>
    #include <thread>
    #include <mutex>
    #include <future>
    using namespace std;
    
    unsigned int cnt; //warum war cnt static?
    mutex mtx;
    
    void inkrement(){
      lock_guard<mutex> waechter{mtx}; //soweit sogut
      ++cnt;
    }
    
    int running(){
      for(int i = 0; i < 100000; ++i) 
        inkrement();
      lock_guard<mutex> waechter{mtx}; //jeder Zugriff auf cnt muss gelockt werden
      cout << cnt << endl;
      return cnt;
    }
    
    int main(){ //main(void) ist 80er-Jahre C
      thread th01(running);
      thread th02(running);
      auto th03 = async(running);
      th01.join();
      th02.join();
      cout << th03.get() << endl;
    }
    

    Jetzt laufen die Threads asynchron ohne Data Race. Nicht dass das was bringen würde, weil eh nur ein Thread gleichzeitig auf cnt zugreifen kann.

    Vielleicht noch was nicht ganz offensichtliches: cout ist lock-geschützt, das heißt prinzipiell kannst du von 2 Threads gleichzeitig auf cout ausgeben. Andererseits kriegst du dann doofe Interleavings. Zum Beispiel wenn du gleichzeitig "Hello" und "World" ausgibst, dann kann "HWeorllldo" raus kommen. In meinem Beispiel schützt der lock in Zeile 18 auch das cout, sodass das nicht passiert. Das cout in Zeile 29 ist single threaded und hat daher auch kein Problem.



  • Aha, interessant 😃



  • nwp3 schrieb:

    Vielleicht noch was nicht ganz offensichtliches: cout ist lock-geschützt, das heißt prinzipiell kannst du von 2 Threads gleichzeitig auf cout ausgeben. Andererseits kriegst du dann doofe Interleavings. Zum Beispiel wenn du gleichzeitig "Hello" und "World" ausgibst, dann kann "HWeorllldo" raus kommen.

    Hä?
    Was heist das?
    Sind die Std-Streams nun gelockt oder nicht?

    Oder meisnt du damit, dass die sich durch gegenseitiges Threading nicht in einem blöden Zustand bringen, aber dennoch unter Umständen Buchstabensalat ausgeben (so wie du angedeutet hast)?



  • Skym0sh0 schrieb:

    Oder meisnt du damit, dass die sich durch gegenseitiges Threading nicht in einem blöden Zustand bringen, aber dennoch unter Umständen Buchstabensalat ausgeben (so wie du angedeutet hast)?

    Yup, genau das. Locks schützen nicht vor allem bzw. wenn sie es täten, dann wäre die performance weg. Das nennt sich determinacy race. Zum Beispiel ist in meinem Beispiel undefiniert, ob zuerst th01, th02 oder th03 cnt ausgeben, trotz lock. Ob so ein Race ein Problem ist muss die Anwendung definieren. Ich weiß nicht an welcher Stelle cout lockt, wahrscheinlich an den vordefinierten operator<<()-Funktionen, sodass cout << "hello world" an einem Stück ausgegeben werden sollte, aber cout << "hello " << "world" nicht unbedingt, aber das könnte ich nicht belegen. Hier steht noch was.


Anmelden zum Antworten