Threadingfrage



  • Angenommen ich starte mein Konsolenprogramm und erstelle dort einen Thread.

    dieser Thread macht nur was wenn

    while(ptrKlasse->isActive()) // die Klasse active ist
    {
    
    [...]
    
    }
    

    Der andere Thread (also das Hauptprogramm) nimmt Eingaben entgegen, z.B.

    Programm beenden: 1

    Wird wie oben eine 1 eingeben so wird ptrKlasse->active(false) gesetzt damit der Thread sich beendet.

    Preisfrage: Kann das Programm abstürzen weil bool active gesetzt wird und gleichzeitig versucht wird den Wert abzufragen und es steht noch kein gültiger Wert in der Variablen 😕 ?



  • C++ kennt keine Threads, deshalb kann man das innerhalb von Standard C++ nicht beantworten. Deine Plattform sollte aber definieren, was in diesem Fall geschieht.

    Ich würde nicht davon ausgehen, dass das Programm abstürzt. In der Realität kann es aber gut passieren, dass dein Thread nie etwas von der Änderung mitkriegt, da der Compiler aus der while Afrage eine if Abfrage mit Endlosschleife macht.

    Nimm mal das folgende Programm:

    extern "C" {
    #include <pthread.h>
    #include <unistd.h>
    }
    #include <iostream>
    
    bool active = true;
    
    extern "C" void * thread_start( void *)
    {
       sleep(1);
       active = false;
       return 0;
    }
    
    int main()
    {
       pthread_t tid;
       pthread_create(&tid, 0, thread_start, 0);
    
       unsigned long counter = 0;
    
       while (active == true) {
          ++counter;
       }
    
       std::cout << counter << std::endl;
       pthread_join(tid, 0);
    }
    

    Und dazu mal das Log:

    [ponto@burns ponto]$ g++ loop.C -Wall -W -pedantic -pthread
    [ponto@burns ponto]$ ./a.out
    553494581
    [ponto@burns ponto]$ g++ loop.C -Wall -W -pedantic -pthread -O3
    [ponto@burns ponto]$ ./a.out
    

    Und wenn der Rechner nicht rebootet wurde, so läuft das Programm noch heute.

    Also: Benutz einfach eine der vielen Synchronisationsmöglichkeiten. Mutexe, Condition Variablen, Pipes, ...



  • Nein, das kann nicht sein. Die Zuweisung auf Basistypen ist eine atomische Operation, die nicht vom Scheduler unterbrochen werden kann.



  • 0xdeadbeef schrieb:

    Nein, das kann nicht sein. Die Zuweisung auf Basistypen ist eine atomische Operation, die nicht vom Scheduler unterbrochen werden kann.

    AFAIK nicht zwingend.
    Etwa 64-Bit-Werten werden auf 32-Bit-CPUs in zwei Befehlen verarbeitet.

    Edit: Der bool-Zugriff ist wohl auf allen gängigen Architekturen atomar (wird aber vom Standard nicht vorgeschrieben, soweit ich weiß).



  • Ah das hilft schon weiter, vielen Dank!



  • 0xdeadbeef schrieb:

    Nein, das kann nicht sein. Die Zuweisung auf Basistypen ist eine atomische Operation, die nicht vom Scheduler unterbrochen werden kann.

    Zum einen ist der Scheduler in diesem Kontext egal, wenn man auf einer Multiprozessormaschine arbeitet. Zum anderen ist das von Standard nicht gewährleistet.

    Ich kann mir gut vorstellen, dass das Lesen und Schreiben eines char auf einer 32Bit Maschine nicht atomar ist. Um ein Byte zu schreiben, werden eventuell 4 Bytes gelesen, eines davon verändert und alle vier wieder zurückgeschrieben.



  • 0xdeadbeef schrieb:

    Nein, das kann nicht sein. Die Zuweisung auf Basistypen ist eine atomische Operation, die nicht vom Scheduler unterbrochen werden kann.

    dann muss aber die zuweisung aus einem einzigen assembler-befehl bestehen. sonst kann's doch unterbrochen werden



  • Das Lesen von integralen Datentypen wird prinzipiell als atomar angenommen (ob der Standard das auch garantiert, kann ich jetzt nicht sagen). Das Schreiben jedoch grundsätzlich nicht. So kann es zum Beispiel aus den Operationen "Wert ins Register laden" und "Wert an Speicheradresse XYZ schreiben" zusammengesetzt sein. Dabei kann einem der andere Thread natürlich schon reinpfuschen. Das kann zum klassischen Producer-Consumer-Problem führen, wo der Consumer sich genau in dem Moment schlafen legt, als der Producer schreibt und die Änderung so niemals sieht.

    Daher wie schon gesagt wurde - eine Möglichkeit zur Synchronisation nutzen.



  • net schrieb:

    dann muss aber die zuweisung aus einem einzigen assembler-befehl bestehen. sonst kann's doch unterbrochen werden

    Das ist richtig. Der nennt sich in der Regel "mov-irgendwas".

    OK, ich sollte mich klarer ausdrücken - das Schreiben in einen Basistypen ist auf jeden Fall atomisch commitet. Das heißt, genau ein Maschinenbefehl nimmt die tatsächliche Änderung vor, und es gibt keinen Moment, in dem die Variable einen halben bool enthält. Dementsprechend wird bei dem Codestück unten das Programm nicht abstürzen, selbst wenn das timeslice ausgerechnet zwischen "ins Register laden" und "veränderten Wert zurückschreiben" zuende gehen sollte.



  • Evtl. achtet der Compiler auch darauf, wenn man volatile verwendet. bzw. wenn ne Variable wegoptimiert (nicht im Speicher gehalten) wird, weil sie nicht volatile ist, funktioniert es schon mal sicher nicht gescheit.



  • 0xdeadbeef schrieb:

    OK, ich sollte mich klarer ausdrücken - das Schreiben in einen Basistypen ist auf jeden Fall atomisch commitet. Das heißt, genau ein Maschinenbefehl nimmt die tatsächliche Änderung vor, und es gibt keinen Moment, in dem die Variable einen halben bool enthält.

    klar, wenn ein thread nur lesend zugreift und der andere die variable verändert ist alles harmlos. gefährlich wird's, wenn zwei threads 'read-modify-write' machen. da hilft dann kein noch so schöner c-standard mehr, der atomare schreiboperationen vorschreibt 😉

    Optimizer schrieb:

    Evtl. achtet der Compiler auch darauf, wenn man volatile verwendet. bzw. wenn ne Variable wegoptimiert (nicht im Speicher gehalten) wird, weil sie nicht volatile ist, funktioniert es schon mal sicher nicht gescheit.

    genau. volatile muss da rein.



  • Das Schreiben ist atomar. Um etwas zu schreiben, muss man aber erstmal irgendwoher die Information beschaffen, die geschrieben werden soll. Damit ist ein Schreibvorgang immer Lesen+Schreiben. Natürlich ist das Schreiben alleine (eines primitiven Datentyps) atomar, d.h. es wird nie eine 'halber' Wert im Speicher stehen. Zwischen Lesen und Schreiben kann man aber unterbrochen werden. Selbst bei einer atomaren Prozessor-Operation kann einem ein zweiter Prozessor in die Quere kommen. Und wenn ich mich recht erinnere, gibt es kein mov $xxxxxxxx $yyyyyyy.

    Hat sich schonmal jemand gewundert, warum es unter Windows die InterlockedXXXXXX (Increment, Decrement, etc) Funktionen gibt (arbeiten alle auf 32-Bit-Werten, die in einer Operation geschrieben werden), aber kein InterlockedRead? Weil das InterlockedRead nicht nötig ist, die anderen aber schon.



  • 7H3 N4C3R schrieb:

    Um etwas zu schreiben, muss man aber erstmal irgendwoher die Information beschaffen, die geschrieben werden soll.

    Konstanten musst du nicht vorher irgendwoher lesen, und in diesem Fall geht es um die Zuweisung einer Konstanten.



  • 0xdeadbeef schrieb:

    7H3 N4C3R schrieb:

    Um etwas zu schreiben, muss man aber erstmal irgendwoher die Information beschaffen, die geschrieben werden soll.

    Konstanten musst du nicht vorher irgendwoher lesen, und in diesem Fall geht es um die Zuweisung einer Konstanten.

    das wär' sowieso wumpe wenn nur ein thread schreibzugriffe macht



  • 0xdeadbeef schrieb:

    Konstanten musst du nicht vorher irgendwoher lesen, und in diesem Fall geht es um die Zuweisung einer Konstanten.

    Und woher soll Compiler den Wert der Konstanze nehmen? Aus dem Hut zaubern? Das ist auch 'nur' eine Zahl, die irgendwo im Speicher steht. Vielleicht im Codesegment, aber immernoch im Speicher.
    Das kopieren 'von Speicheradresse' 'nach Speicheradresse' ist per se keine atomare Operation. Der Speicher ist dumm und kann das nicht alleine, wie auch. Also liest die CPU den Wert ein und schreibt ihn wieder. Womit wir bei zwei Ops sind. Jetzt könnte man noch behaupten, der Wert stünde in einem Register. Aber dann mache ich mir eben mehr Variablen/Konstanzen als ich Register habe und setze alle auf einmal und sitze wieder vor dem selben Problem. Schreiben ist nunmal immer "lesen+schreiben".



  • Heißt das, das Ausführen einer Operation ist nicht atomisch, weil die Operation erst aus dem Speicher gelesen werden muss? Aber das macht ja wieder eine Operation, die erst aus dem Speicher gelesen werden muss, was wieder eine Operation macht, die erst aus dem Speicher gelesen werden muss, was wieder...

    Ich denke, das Code-Segment qualifiziert sich da als Sonderfall.



  • Nicht das Ausführen der Operation ist nicht atomisch. Aber die aus C Sicht atomische Operation 'Wert zuweisen' gliedert sich in zwei aus Prozessorsicht atomische Teilaufgaben. Damit ein Wert in eine Speicherzelle geschrieben werden kann, muss der Prozessor ja wissen, was er schreiben soll. Konstante oder nicht - das gibt es auf der Ebene nicht mehr. Der Wert muss, damit er vom Prozessor an die entsprechnde Stelle geschrieben werden kann, zuerst in ein Prozessorregister geladen werden. Das bedeutet, dass vor der Schreib- eine Lade-Aktion durchgeführt werden muss. Womit wir 2 Ops haben.
    Z.B.
    mov eax, $source
    ///// unterbrechung HIER möglich
    mov $destination, eax

    Es gibt kein
    mov $destination, $source
    eben darum, weil sich damit keine atomische Operation vereinbaren lässt.

    Die Diskussion gehört bald eher ins Assembler-Forum oder sollte mal dort fortgesetzt werden. Ich befürchte, die Assembler-Leute schauen hier weniger rein 😉



  • Nicht das Ausführen der Operation ist nicht atomisch. Aber die aus C Sicht atomische Operation 'Wert zuweisen' gliedert sich in zwei aus Prozessorsicht atomische Teilaufgaben. Damit ein Wert in eine Speicherzelle geschrieben werden kann, muss der Prozessor ja wissen, was er schreiben soll. Konstante oder nicht - das gibt es auf der Ebene nicht mehr. Der Wert muss, damit er vom Prozessor an die entsprechnde Stelle geschrieben werden kann, zuerst in ein Prozessorregister geladen werden. Das bedeutet, dass vor der Schreib- eine Lade-Aktion durchgeführt werden muss. Womit wir 2 Ops haben.
    Z.B.
    mov eax, $source
    ///// unterbrechung HIER möglich
    mov $destination, eax

    Es gibt kein
    mov $destination, $source
    eben darum, weil sich damit keine atomische Operation vereinbaren lässt.

    Die Diskussion gehört bald eher ins Assembler-Forum oder sollte mal dort fortgesetzt werden. Ich befürchte, die Assembler-Leute schauen hier weniger rein 😉



  • Dann ist es seltsam, dass der gcc überhaupt Code kompiliert kriegt. Der schreibt dauernd Anweisungen a la

    movl $0 -4%(ebp)
    

    in den ASM-Code, weist die Konstanten also direkt zu...


Anmelden zum Antworten