Multithread lock read/write



  • ein Byte kann doch auch ein Load/Store sein - oder - wie soll das Threadsafe sein?



  • Nochmal kurz:
    Es soll von den ankommenden Daten keine "übersehen" werden.
    Es muss nicht jede angekommende Nachricht ausgegeben werden (z.b nur der aktuell gespeicherte Wert bei der Abfrage alle 1000ms)

    -> Grund: Sollte ein Wert einmalig kommen und ich übersehe ihn, dann sieht es so aus als ob nichts angekommen wäre.

    Erklär doch mal ganz genau was es für Daten sind - was du damit machst (über die Zeit - filtern, summieren) und wie diese angezeigt werden sollen (der "letzte" Wert selbst oder der Durchschnitt oder was)



  • Gast3 schrieb:

    ein Byte kann doch auch ein Load/Store sein - oder - wie soll das Threadsafe sein?

    Wie meinst du das? Ich stelle mir das Problem in etwa so vor:

    char a;
    // Thread 1:
    while(1) {
     a = rechneMitBlue(getValueFromHardware());
    }
    // Thread 2:
    while(1) {
     char tmp = a;
     // tue beliebige dinge mit tmp
     sleep(1000);
    }
    

    Und nachdem ein char immer komplett auf einmal geschrieben wird (im RAM z.b.), kriegt Thread 2 immer einen passenden Wert. Eventuell gibt es neuere Werte, die aktuell noch in irgendeinem Schreibcache liegen, aber es ist immer ein gültiger Wert von nicht allzulange in der Vergangenheit.
    Eventuell denken wir aber auch in unterschiedliche Richtungen oder ich überseh etwas?



  • intern im microcode ist es doch ein load und ein store Befehl die soweit ich weiss unterbrechbar sind - also ist eine bei-byte-ist-es-sicher-Aussage sehr eng begrenzt, da ist man doch schon nah an der atomaren int oder volatile mystik drann



  • Wenn du ein load und store hast (z.b. zahl++), dann geb ich dir recht. das gibt Probleme. Aber die anwendung sieht für mich nach Store-only aus (also der Schreibthread).



  • Gast3 schrieb:

    Nochmal kurz:
    Es soll von den ankommenden Daten keine "übersehen" werden.
    Es muss nicht jede angekommende Nachricht ausgegeben werden (z.b nur der aktuell gespeicherte Wert bei der Abfrage alle 1000ms)

    -> Grund: Sollte ein Wert einmalig kommen und ich übersehe ihn, dann sieht es so aus als ob nichts angekommen wäre.

    Erklär doch mal ganz genau was es für Daten sind - was du damit machst (über die Zeit - filtern, summieren) und wie diese angezeigt werden sollen (der "letzte" Wert selbst oder der Durchschnitt oder was)

    #include <stdio.h> 
    #include <stdlib.h> 
    #include <string.h> 
    #include <gtk/gtk.h> 
    #include <pthread.h> 
    
    typedef struct{ 
    	double wert; 
    	char einheit; 
    } sarray; 
    
    typedef struct _workfields{ 
    sarray arr1[5]; 
    
    } WorkFields; 
    
    void readData(WorkFields *work){ //hiervon Thread erzeugen 
    	//Hardware Initialisierungen... 
    	int i; 
    	uint64_t verarbeiten; 
    	double ergebnis;
    
    	while(1){ 
    	//aus Schnittstelle Daten empfangen und ins Struct *work schreiben
    
    		for(i=0; i<5; i++){
    			//Jedes "i" ist an einen eigenen Sensor gekoppelt
    			verarbeiten = empfangenvonSensor(i);  
    			//verarbeiten: Bits schieben, Multiplikation, Addition, Vergleiche
    
    			ergebnis = ...;
    			//In Struct schreiben
    			work->arr1[i].wert = ergebnis;
    		}
    	} 
    } 
    
    void printData(WorkFields *work){ 
    	//Ausgabe in GUI
    	int i;
    	char str[100];
    
    	while(1){
    		for(i=0; i<5; i++){
    			sprintf(str, "%f %s", work->arr1[i].wert, work->arr1[i].einheit); //Muss so sein, sonst kann ich es nicht in der GUI ausgeben
    			printf("%s\n", str); //Nur als Beispiel, eig. ohne printf in GUI ausgeben
    		}
    		usleep(500);
    	}
    } 
    
    int main(int argc, char *argv[]){ 
    
    	WorkFields *work; 
    	work = g_slice_new(WorkFields); 
    
    	//... weitere Deklarationen
    
    	pthread_t thread1, thread2; 
    
    	pthread_create (&thread1, NULL, readData, work);
    	pthread_create (&thread2, NULL, printData, work); 
    
    	//... GUI erstellen
    
    	gtk_main (); 
    	gdk_threads_leave (); 
    
    	return 0; 
    }
    

    So könnte das Programm in etwa aussehen. Ausgegeben wird nur der aktuell gespeicherte Wert, kein Durchschnittswert o.ä.

    Lg



  • Ok, das dürfte zu komplex sein.
    Dann hätte ich hier mal einen Vorschlag (Stichwort: nichtblockierende Synchronisation). Aber nicht blind übernehmen. Es sollte nochmal jemand drüberschauen, ob es nicht doch einen Fall gibt, der es kaputt macht.

    Die Idee ist:
    Der Schreibthread schreibt immer.
    Der Lesethread liest, wenn er denk, der Zeitpunkt ist gut. Wenn während des Lesens aber neue werte geschrieben wurde, wird das Ergebnis einfach verworfen und später erneut versucht.

    // Globale Variablen (volatile, damit garantiert richtiger wert gelesen wird)
    volatile bool flag_Running; // gibt an, ob Schreibthread gerade schreibt
    volatile bool flag_Dirty; // Gibt an, ob werte schon gelsen wurden
    
    // Thread Schreiben:
    while(1) {
        flag_Running = true;
        flag_Dirty = true;
            // Der normale Schreibkram
            // Eventuell sollte die Berechnung und alles vorher gemacht werden, um die Schreibzeit zu minimieren
        flag_Running = false;
    }
    
    // Thread Lesen:
    while(1) {
        do {
            // wir warten bis gerade nicht geschrieben wird
            while(flag_Running) { sleep(10); }
            // danach erkennen wir wieder neue Daten
            flag_Dirty = false;
            // verdammt, es wird schon wieder geschrieben. Dann doch nochmal warten
            if (flag_Running) continue;
                // jetzt koenen wir endlich lesen.
                // erstmal alles schnell in  lokale variablen lesen. danach den sprintf kram machen
        // falls Dirty gesetzt wurde waehrend des lesen, haben wir Muell gelesen eventuell. Dann nochmal das ganze von vorne
        } while (flag_Dirty);
        // Uns ist es gelungen alles zu lesen
        printf(...);
        sleep(1000);
    }
    

    Das ganze klappt aber nur, wenn deine Timings passen. D.h. der Schreibthread muss lange genug außerhalb des flag_Running==true Bereichs sein. Und Das Lesen muss zügig gehen. D.h. z.B. in 99% der Fälle schafft es der Lesethread die Daten zu Lesen, bevor die neuen Daten vorbereitet wurden. Und in dem restlichen 1% muss er es halt onchmal versuchen.



  • // Globale Variablen (volatile, damit garantiert richtiger wert gelesen wird)
    volatile bool flag_Running; // gibt an, ob Schreibthread gerade schreibt
    volatile bool flag_Dirty; // Gibt an, ob werte schon gelsen wurden

    das ist nicht threadsicher und dein volatile verhindert nur eine moegliche wegoptimierung (da der kompiler denken koennte die Thread-proc werden nicht genutzt) - oder was meinst du mit "volatile, damit garantiert richtiger wert gelesen wird"? soviel zur volatile Mystik



  • In welchem Fall würde es denn kaputt gehen? Kannst du einen konkreten Ablauf angeben?


  • Mod

    DerBaer2 schrieb:

    In welchem Fall würde es denn kaputt gehen? Kannst du einen konkreten Ablauf angeben?

    In den gleichen Fällen, wie wenn dort kein volatile stünde. volatile hat mit Threadsicherheit einfach überhaupt nichts zu tun. Sonst wäre es schließlich nicht nötig gewesen, für C11 richtige memory barriers, atmoics und vieles mehr einzuführen.



  • die volatile Lüge wird seit Jahren gerne und unwissend kopiert und dann auch noch weiter gelehrt - volatile sichert die Variable nur gegen Wegptimierung ab - sonst gar nichts - keine Magie und Mystik in Kombination mit Threads



  • Was ist volatile genau?
    Von der Wortherkunft her bedeutet es "veränderlich", "flüchtig". Non-volatile (also das, womit du am Häufigsten arbeitest) ist demnach nicht veränderlich.

    Im Kontext der Programmierung bedeutet dies: eine volatile Variable kann sich jederzeit ändern. Einfach so. Von einem Cycle auf den anderen. Auch, wenn der Compiler nicht einmal einen Zugriff auf die Variable im Code sieht - gerade war x noch 1, jetzt ist es 2, und dazwischen hat niemand eine Zuweisung gesehen.

    Natürlich ist eine Zuweisung/Veränderung erfolgt, aber sie ist für den Compiler nicht sichtbar. Und deswegen muss der Compiler jedes Mal die Variable vom Speicher holen, wenn auf sie zugegriffen wird. Vom lahmen Hauptspeicher jetzt. Der RAM. Früher war das der lange, weite Weg über die Northbridge. Und Optimizer haben versucht, diesen langen weiten Weg zu ersparen, indem sie oft verwendete Werte in den Registern gelassen haben. Und das geht ja so lange in Ordnung, wie der Wert in der Speicherstelle mit dem Wert im Register kongruent ist.

    Aber wenn jetzt, sagen wir mal, von irgendwo her ein Signalhandler/Interruipt, der einfach mal alles pausiert, aufgerufen wird, und x auf 2 ändert, und dein Programm dann weiter ausgeführt wird, dann ist der Wert im Register fehlerhaft. Weil im Speicher ja jetzt 2 steht. Das weiß der Code aber nicht, deswegen lädt er nicht nach.

    Deswegen kannst du mit volatile sagen, dass der Compiler die Variable nicht in ein Register laden soll. Sondern immer schön direkt aus dem Speicher. Das ist natürlich langsamer als ein Register. Aber besser als abrauchender Code.

    Jetzt habe ich allerdings auch eine Frage: volatile verhindert nicht, dass eine Variable in den CPU-Cache geladen wird - nur in ein Register oder eine "schönere Speicherstelle", korrekt? Damit wären volatile Variablen ein wenig langsamer als vollständig durchoptimierte Variablen, aber der Effekt wird doch drastisch abgemildert.



  • Danke für die zahlreichen Antworten.

    Ich weis jetzt leider noch nicht ganz, ob ich einen Mutex/anderen Lock benötige oder nicht.

    Ich möchte noch ergänzen das ich herausgefunden habe, das meine Empfangshardware einen eigenen Puffer verbaut hat.
    Somit ist es vielleicht doch ok, wenn er kurz auf den Mutex unlock warten müsste.

    //Initialisierung
    
    while(1){
    
       sleep(3);
       //während dem Wartezeitpunkt Sensorwerte erhalten
    
       wert = empfangewert();
    
    //...
    }
    

    Ich habe mal testweise während dem Sleep, also noch bevor ich beim schritt "wert = empfangewert();" gewesen bin, 5 Temperaturwerte manuell direkt hintereinander geschickt (kann ich machen). Die Werte kamen dann alle nacheinander im 3 sek Abstand an. --> Somit mussten die irgendwo Hardwareseitig zwischengespeichert gewesen sein.

    Des Weiteren kann ich angeben, das er z.B. den letzten Wert den er innerhalb von 500ms empfangen hat, weiter senden soll. Also muss er ja auch hier die empfangenen Werte puffern um den letzten weiter geben zu können.

    --

    Mein Programm funktioniert halt auch ohne einen Mutex. Solange es dadurch nicht zu abstürzen kommt und kein Wert verschwindet wäre alles ok.

    Benötige ich also einen Lock...?

    Gruß

    edit: Möchte noch eine andere Frage zum Verständnis loswerden:
    Mal angenommen in meinem Struct, wie beschrieben, wird auf die Variable "wert" nur von Thread1 zugegriffen (read/write) und die 2. Variable "einheit" wird nur von Thread2 verwendet (read/write). Dann bräuchte man keinen Lock, da es ja unterschiedliche Variablen sind. Oder stimmt das nicht, da sie sich im selben Struct befinden?



  • Ich möcht' dich eigentlich schlagen, weißt du das? Weil das alles nicht im OP steht und ich jetzt mühselig die einzelnen relevanten Informationen rauspicken muss.

    Wo steht im OP, dass die Werte von Hardware kommen? Steht da nicht.
    Was sind das für Sensoren, der man Werte schickt? Hast du selbst gesagt, dass du dir 5 mal hintereinander "geschickt hättest". Hat die Hardware einen Eingang, über den du den jetzigen Wert überschreiben kannst bzw. pushen? Wenn nicht, was für Werte kamen nach 3 Sekunden an?

    Das ist alles sehr unausgegoren und zeigt, dass du nicht wirklich verstehst, was du da machst.

    Wenn ich mit so einer Aufgabe betreut worden wäre, wäre mein Ansatz erst mal der folgende:

    - muss ich beim meinem Eingang (also dem Ausgang der Hardware) pollen? Pollen ist immer blöd. Gibt einen Grund, warum man bei Interrupts nicht pollt, sondern die IRQs den (A)PIC verarbeiten lässt.
    - wenn ich Ereignissteuerung machen kann, super! Lege ich fest, dass ich bei Nachrichteneingang aufgeweckt werden will, und lass den Prozess dann schlafen. Und wenn was reinkommt, verarbeite ich das. Das baue ich in einem Thread - ach, das baue ich dir direkt in main .
    - wenn ich pollen muss, Scheiße. Schaue ich nach, gibt es irgendwo Dokumentation dazu, ob die Harware mir eine Garantie macht, in welchem Intervall ein Update erfolgt. Mit einem Intervall von 1 ms kann man bereits ordentlich arbeiten, das Holen und Verarbeiten des Wertes wird weniger als 1 ms dauern. Ausgabe hängt von der Implementierung ab und kann länger als 1 ms dauern, aber dann ist das nicht mehr dein Fehler. Aber ständig und immer zu pollen, ergibt wenig Sinn. Zumal ich auch denke, dass die Hardware gar nicht so schnell ist.

    Was das ganze komplizierter macht, ist, dass du GTK verwendest. Fensteranwendungen sind ein wenig komplexer und müssen ständig reagieren. In beiden Fällen (Pollen und Nicht-pollen) brauchen wir dann zwei Threads. Der erste, der Hauptthread, ist mit dem Fenster beschäftigt, für den macht man nicht mal manuell einen Thread auf. Der zweite Thread pollt auf die Werte oder wird über neue Werte in Kenntnis gesetzt, und wenn es ein Update gab, lockt sich der zweite Thread den Mutex auf das GTK-Fenster ("gtk multithreading", einfach mal googlen), schreibt die Werte, und gibt den Mutex dann wieder frei.

    Den Mutex brauchst du hier übrigens, weil das Fenster eine gemeinsam genutzte Ressource ist. Da hilft kein Wenn und Aber - wenn's bei dir funktioniert, dann lass einfach mal laufen und wundere dich nicht, wenn das Fenster plötzlich einfriert und keine Updates mehr raushaut. Das passiert, weil das GTK-Framework mit seinem Status durcheinanderkommt.

    Also: der zweite Thread kümmert sich direkt um Einlesen und Schreiben. Der erste kümmert sich darum, dass das GTK-Fenster ordentlich angezeigt wird. Zwei Threads, zum Lesen und zum Schreiben, das würde ich ganz sein lassen. Dann hast du nämlich drei Threads, die untereinander synchronisiert werden müssen, und das ist noch schöner.
    Und wenn du gar kein GTK hast, dann brauchst du nicht mal Threads zu verwenden. Dann geht das alles in einem Thread.

    ABER das ist MEINE Implementierung - weil wir halt nicht wissen, wie das genau aussieht und du auch keine leicht zu erfassenden Informationen anbietest.

    JohnDillinger schrieb:

    Mal angenommen in meinem Struct, wie beschrieben, wird auf die Variable "wert" nur von Thread1 zugegriffen (read/write) und die 2. Variable "einheit" wird nur von Thread2 verwendet (read/write). Dann bräuchte man keinen Lock, da es ja unterschiedliche Variablen sind. Oder stimmt das nicht, da sie sich im selben Struct befinden?

    Undefiniert, Es kann sein, dass du auf einer CPU arbeitest, die nur, sagen wir mal, 8-Bytes auf einmal speichern und laden kann. Und wenn du zwei 32-Bit-ints direkt nebeneinander im Struct hast, wird der Compiler keine Padding-Bytes einfügen.
    Thread 1 will jetzt Wert 1 updaten, aber da die CPU nur 8 Byte atomar speichern kann, lädt diese halt Wert 2, generiert einen Wert, der beim Schreiben Wert 1 überschreibt, Wert 2 "praktisch" auch, aber da der Wert in der Theorie des Compilers der gleiche ist, merkst du das ja nicht (haha), und fertig ist der Lack.

    Problem ist, dass das nicht atomar ist. Wenn Thread 2 jetzt Wert 2 überschreibt, während Thread 1 noch am Werkeln ist, überschreibt Thread 1 am Ende noch das Update von Thread 2. Deswegen sind atomare Anweisungen bei asynchroner Ausführung ja so wichtig.



  • Ähem, dann wäre ich von volatile richtiggehend enttäuscht.
    Oder andersrum gefragt, was hätte volatile für einen Sinn, wenn es nicht voll auf die Targetadresse durchgreift?
    Denn ein anderer Thread kann ja auch nur auf die bekannte Adresse schreiben, woher soll der wissen, daß der andere nur im Cache rumnudelt?

    CPUs sind keine Autisten.
    volatile heißt, guck' nochmal nach, auch wenn der Code sagt, daß man da vorher 2 reingetan hat, schau' nach, ob wirklich 2 drinsteht.
    Und natürlich darf dann das der Compiler nicht wegoptimieren, den Rest macht der Memorycontroller, der bei Inkonsistenz nachlädt.

    Zumindest hab' ich das so verstanden, so genutzt und es hat so funktioniert. 🙂



  • Ja klar heisst volatile "durch bis zum Metall".
    Was volatile nicht heisst/macht:
    * atomic
    * memory ordering



  • olatile heißt, guck' nochmal nach, auch wenn der Code sagt, daß man da vorher 2 reingetan hat, schau' nach, ob wirklich 2 drinsteht.
    Und natürlich darf dann das der Compiler nicht wegoptimieren, den Rest macht der Memorycontroller, der bei Inkonsistenz nachlädt.

    Zumindest hab' ich das so verstanden, so genutzt und es hat so funktioniert.

    stimmt ja auch - nur das eben dadurch trotzdem keine Threadsicherer Zugriff realisiert ist, erstmal nur das überhaupt der Inhalt erreichbar ist - aber nicht ob der auch "im" richtigen Zustand ist



  • und darum schreibe ich auch Lüge oder Mystik - viele missbrauchen volatile als - Threadzugriffssicherung - obwohl es keine ist, es läuft durch pures Glück trotzdem - ist aber einfach total falsch



  • dachschaden schrieb:

    ...

    Hallo,

    sorry das ich mich nicht mehr gemeldet habe.

    Ich habe mir deinen Ansatz nochmal durch den Kopf gehen lassen und ein wenig gegoogled und nun eine stabile Lösung gefunden.

    Ich habe den Thread der mir in der GUI die Werte aktualisiert hat rausgeworfen und lasse das über den Haupthread (=GTK Thread) laufen. Dieser Hauptthread ruft mittels einer Timeout Funktion alle (z.B.) 500ms eine Funktion auf und diese verändert dann die GUI.

    Ein Sensor ist für mich eine Hardware. Jedenfalls läuft dieser wie gehabt in einem eigenen Thread. Beziehungsweise musste ich für jeden Sensor einen eigenen machen, da ich herausgefunden habe, dass das Programm stoppt bis ein neuer Wert angekommen ist. Was zur Folge hätte, das die anderen Sensoren in dieser Wartezeit nichts mehr empfangen hätten.

    Jedenfalls habe ich dann noch meinen Code optimiert, sodass pro Thread nur noch ein einziger Lese/Schreibzugriff auf das Struct nötig ist. Diese Zugriffe habe ich mittels eines Mutex jeweils gelockt.

    Habe mit dieser Lösung keinen speziellen "GTK-Lock" gebraucht, da nur der Hauptthread die GUI updatet.

    Es funktioniert perfekt!

    Danke 🙂



  • JohnDillinger schrieb:

    Es funktioniert perfekt!

    Danke 🙂

    Ich vermute mal, alle Beteiligten hier atmen jetzt tief durch und hoffen dass Dein Temperaturprogramm nicht "systemrelevant" ist - etwa zur Steuerung eines Atomkraftwerks...
    😃


Anmelden zum Antworten