Multithread lock read/write



  • Hallo,

    ich versuche es kurz zu machen:

    Ich habe ein Struct, 2 Threads und eine Main-Funktion.

    Die main-Funktion übergibt den Pointer auf das Struct, an die beiden Threads.

    Die beiden pThreads laufen jeweils permanent durch -> while(1).

    Thread1 (schreiben) soll eine Variable aus dem Struct aktualisieren (z.b. "struct->zahl++")

    Thread2 (lesen) soll den aktualisierten Wert jede Sekunde ausgeben (z.B. "printf("%d\n", struct->zahl)")

    Das funktioniert auch soweit mit sowie ohne den Einsatz von einem Mutex.
    Ohne Mutex natürlich schneller, es kommt also schneller zu höheren Zahlen, da die Addition schneller abläuft.

    Fragen:
    1. Kann ich das ganze ohne Mutext betreiben? Da ich ja nicht aus 2 Threads auf eine Variable schreibe sondern nur von einer. Und von der anderen lese.
    Nachteilig an einem "normalen" Mutex finde ich, dass der Schreib-Thread (Thread1) während Thread2 am lesen ist, kurz angehalten wird. Das möchte ich nicht! Andersrum wäre es kein Problem.

    2. Es gibt "pthread_rwlock" locks. Ich habe aber noch nicht das gefunden was ich benötige, nämlich das der Schreib-Thread vorrang hat vor dem Lesethread. Der Schreibthread soll also nie warten müssen. Der Lesethread darf ruhig kurz angehalten werden.

    Lg



  • -dein IO/printf ist zig-tausendmal langsamer als deine Addition

    -nach welchem Prinzip sollten den der print-thread als "weniger" wichtig vom OS eingestuft werden? Wenn du ein "nachlaufendes" Konsolenupdate haben willst musst du das schon selber machen z.B. alle n ms/sek einen Wert holen und nicht 100% volle Power

    -vermeide IO in Threads - besonders mit der Konsole

    wenn du kein IO in dieser Form hast gibt es auch nicht diese Bremse

    vergleichbar mit eine (nicht wegoptimierte) Schleife die addiert und in einem Fall mit oder ohne printf arbeitet - das ist auch sehr langsam

    was ist denn dein eigentliches Problem - weil das ist zu klein um sinnvoll optimierbar zu sein



  • Gast3 schrieb:

    -dein IO/printf ist zig-tausendmal langsamer als deine Addition

    -nach welchem Prinzip sollten den der print-thread als "weniger" wichtig vom OS eingestuft werden? Wenn du ein "nachlaufendes" Konsolenupdate haben willst musst du das schon selber machen z.B. alle n ms/sek einen Wert holen und nicht 100% volle Power

    -vermeide IO in Threads - besonders mit der Konsole

    wenn du kein IO in dieser Form hast gibt es auch nicht diese Bremse

    vergleichbar mit eine (nicht wegoptimierte) Schleife die addiert und in einem Fall mit oder ohne printf arbeitet - das ist auch sehr langsam

    was ist denn dein eigentliches Problem - weil das ist zu klein um sinnvoll optimierbar zu sein

    Hallo, danke.

    Mein eigentliches Projekt sieht vor, das ich über externe Hardware (Sensoren...) permanent Werte geliefert bekomme. Damit ich keine Wert "verpasse", meinte ich eben sollte dieser Thread am besten nie pausiert werden.
    Er hat einen Initialisierungsteil und läuft dann mittels while(1) permanent durch. Die Addition war auch hier wieder nur ein Beispiel. Die Werte die ich bekomme verarbeite ich noch vorher (z.B. Umrechnung von Kelvin in grad Celcius ...) und schiebe sie dann in das Struct. (Thread1)

    Diese Werte möchte ich dann z.B. alle 500-1000ms aus dem Struct holen und über eine GUI (erstellt in GTK+) ausgeben (die Werte die zwischenzeitlich aktualisiert werden, sind natürlich "verloren", aber das ist ja egal). (Thread2)
    Ich gebe eigentlich nichts über printf aus, das war nur ein kleines Beispiel. Ich nehme aber mal an, das die Ausgabe über eine GUI nicht schneller ist wie mit printf?

    so sieht meine vereinfachte print Funktion dann aus:

    void print(struct1 *bla){
    	while(1){
    		printf("%d\n", bla->zahl);
    		usleep(1000000);
    	}
    }
    


  • zu fuellst ueber deinen Sensor-Abgreif-Thread einen Ringpuffer
    und informierst alle ms/sek den GUI-Thread (Message/Event) das es was zu holen gibt - das ist es entkoppelt und beide koennen so schnell wie sie eben koennen
    - also ein wenig entkoppelt



  • Gast3 schrieb:

    zu fuellst ueber deinen Sensor-Abgreif-Thread einen Ringpuffer
    und informierst alle ms/sek den GUI-Thread (Message/Event) das es was zu holen gibt - das ist es entkoppelt und beide koennen so schnell wie sie eben koennen
    - also ein wenig entkoppelt

    OK hört sich nach einer Alternative an, werde ich mir anschauen.

    So wie ich es beschriebe habe geht es wohl nicht oder?

    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.



  • Was für ein Datentyp ist denn "zahl"?
    Sollte es sich um 1 Byte handeln (oder je nach Architektur auch mehr), brauchst du das Mutex gar nicht. Sofern der Schreib- bzw. Lesebefehl atomar alle Bytes/Bits auf einmal lesen/schreiben kann, gibt es kein Problem ohne Mutex.



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


Log in to reply