Multithreading: Mit Mutex lock und unlock und Signalen arbeiten (C-Programmierkurs) Frozen Code https://www.youtube.com/watch?v=242rAD6Efgw Transkript (automatisch erstellt) 0:00 hallo zusammen in diesem Video schauen wir uns weiter threats an und da vor allem wie wir data races und Race Conditions verhindern können und wie 0:06 threats sich zueinander synchronisieren können ein Link zu dem hier gezeigten qucode findet ihr natürlich wieder in der Videobeschreibung schauen wir uns 0:14 mal ein einfaches Beispiel an in diesem Programm gibt es zwei Funktionen die gleich nacheinander aufgerufen werden count up und Countdown in diesen 0:23 Funktion passiert nicht sehr viel die Funktionen zählen einfach die Variable counter in einer Schleife 1us mal hoch bz Weise runter dabei sollte man 0:32 beachten dass das eine globale Variable ist das heißt dass die beiden Funktionen hier die gleiche variable verändern außerdem legen sich die Funktion hier in 0:40 der Schleife immer für 50 Millisekunden schlafen das soll einfach simulieren dass dort noch andere Dinge gemacht werden wir können uns das einfach 0:48 vorstellen dass der Teil Code eines nachrichtensystems ist und die countup Funktion die Nachrichten empfängt und abspeichert dann den Counter um eins 0:56 erhöht um zu signalisieren dass es eine neue Nachricht gibt und die down Funktion verarbeitet dann diese Nachricht weiter und zählt am Ende 1:03 wieder runter um zu signalisieren dass es keine zu verarbeitende Nachricht mehr gibt in der mainfunktion setzen wir einfach den Counter initial auf Null 1:11 führen dann count up und dann Countdown aus und geben dann aus auf welchem Wert der Counter dann steht wenn wir das Programm jetzt ausführen dann dauert das 1:20 einen kleinen Moment und am Ende sehen wir dann dass der Counter tatsächlich auf Null steht wie erwartet das wurde ja schließlich genauso oft hochgezählt wie 1:28 runter wir können auch einmal einen Timer nutzen um zu messen wie lange die Ausführung dauert tatsächlich dauert das ja recht lang um die Ausführung zu 1:36 beschleunigen können wir uns aber daran erinnern dass wir auch Funktion parallel ausführen können indem wir sie in eigenen Threads starten wie man das 1:44 macht habe ich im letzten Video ausführlich gezeigt den Link dazu findet ihr noch mal in der Videobeschreibung wir legen also wieder zwei Variablen vom 1:51 Typ pthread T an und starten dann die beiden Funktionen mit der Funktion pthread create da müssen wir jetzt keine Argumente übergeben weil die beide 2:00 Funktion ja auf die globale Variable Counter zugreifen können und das ist ja eigentlich die einzige Variable die die beiden Funktionen benötigen um eben zu 2:08 arbeiten danach warten wir noch mit der Funktion pthread join bis beide Funktionen auch tatsächlich komplett ausgeführt wurden und geben danach auch 2:16 wieder den Wert von Counter aus wenn wir jetzt das Programm wieder kompilieren und wieder mit dem Timer starten dann sehen wir dass das Programm auf jeden 2:25 Fall deutlich schneller läuft das ist natürlich super aber wir sehen auch dass das Programm jetzt gar nicht mehr unbedingt das richtige Ergebnis ausgibt 2:33 das Ergebnis sind jetzt völlig zufällige mal positive mal negative Werte aber nicht zuverlässig Null das Problem dabei sind sogenannte data races das passiert 2:42 weil die Befehle counter++ und counter-us Minus keine sogenannten atomaren Befehle sind eigentlich bestehen diese Befehle nämlich aus drei 2:51 einzelnen Befehlen zuerst wird die Variable aus dem Speicher in die CPU geladen dann wird die Variable von der CPU erhöht oder verringert und als als 2:59 letztes wird das Ergebnis davon wieder in den Speicher geschrieben und weil wir jetzt zwei parallel laufende Threads haben zwischen denen jederzeit hin und 3:07 her geschaltet werden kann kann eben auch zwischen diesen Einzelschritten hin und her geschaltet werden es kann jetzt also passieren dass die countervariable 3:15 gerade auf Null steht die countup Funktion die variab liest auf eins verändert aber bevor sie die wieder in den Speicher schreibt könnte jetzt 3:23 Countdown ausgeführt werden und in dieser Funktion kann die Variable auch wieder aus dem Speicher gelesen werden indem sie gerade noch auf Null steht 3:30 dann wird sie dort um eins verringert und wieder in den Speicher geschrieben wenn jetzt die countup Funktion weiter ausgeführt wird dann wird also der dort 3:40 aktuelle Wert in den Speicher geschrieben also steht jetzt die Variable Counter auf 1 obwohl sowohl Countdown als auch countup ausgeführt 3:48 wurden sod dass die Variable eigentlich auf Null stehen müsste das ganze können wir aber natürlich verhindern indem wir den Zugriff auf diese Variable schützen 3:56 s dass immer nur ein Thread gleichzeitig darauf zugreifen kann dazu können wir uns geschützte Bereiche definieren die immer nur von einem Thread zur Zeit 4:04 betreten werden dürfen und das Konzept davon heißt gegenseitiger Ausschluss oder auch mutual exclusion oder kurz Mutex in C gibt es dafür in der pthreads 4:14 Bibliothek auch schon die entsprechenden Funktion und Datentypen um so einen geschützten Bereich zu definieren brauchen wir erst einmal eine sogenannte 4:21 Mutex Variable mit der wir den Bereich identifizieren können dafür gibt es den Datentyp pthread Mutex t ich lege hier so ein eine Variable im globalen 4:31 Speicher an damit dann beide Funktionen auch gleich darauf zugreifen können um diese Variable jetzt benutzen zu können müssen wir diese einmal am Anfang 4:38 unseres Programms initialisieren das können wir mit der Funktion pthread Mutex init machen der wir als ersten Parameter die Adresse einer pthread 4:47 Mutex t Variable übergeben als zweiten Parameter kann man noch weitere Optionen einstellen aber das brauchen wir erstmal nicht und können deshalb Null einsetzen 4:56 um jetzt einen geschützten Bereich zu definieren nutzen wir die Funktion pthread Mutex Log und geben dort die Adresse einer Mutex Variable an wenn wir 5:05 das jetzt in beiden Funktionen direkt vor dem Zugriff auf die countervariable machen verhält sich das Programm folgendermaßen die erste Funktion die 5:13 die Zeile mit dem Mutex Log erreicht sperrt diese Mutex variable und kann weitermachen in der Programmausführung wenn jetzt eine andere Funktion auch 5:21 eine Stelle erreicht an der die gleiche Mutex variable geloggt werden soll dann muss diese Funktion dort warten und kann eben nicht weitermachen aber irgendwann 5:30 soll diese Funktion natürlich auch weiterlaufen und das funktioniert erst wenn die Funktion pthread Mutex Unlock ausgeführt wird also angenommen die 5:38 Countdown Funktion hat zuerst den geschützten Bereich erreicht dann logt sie die Mutex variable verändert die Counter varariable und unlockt dann 5:45 diesen Bereich wieder erst dann kann die countup Funktion den geschützten Bereich betreten wenn wir dieses Programm jetzt kompilieren und ausführen dann ist es 5:53 eigentlich immer noch genauso schnell wie vorher aber zusätzlich bekommen wir auch noch ganz sicher das richtige Ergebnis raus 6:01 kommen wir zum nächsten Punkt und zwar zur Synchronisation zwischen threats stellen wir uns mal eine kleine Abwandlung unseres Programms vor vorhin 6:10 haben wir uns ja vorgestellt dass die countup Funktion simuliert dass Nachrichten in unser System hineinkommen und die Countdown Funktion diese 6:16 Nachrichten verarbeitet jetzt stellen wir uns mal vor dass die Countdown Funktion immer auf fünf neue Nachrichten warten muss bevor sie anfängt diese zu 6:24 bearbeiten wenn fünf Nachrichten da sind dann werden sie bearbeitet und der Counter kann wieder um F verringert werden eine einfache Möglichkeit das 6:32 umzusetzen ist mit einer wild Schleife die eben immer prüft ob Counter noch kleiner als 5 ist wenn das der Fall ist dann passiert hier im Schleifenkörper 6:41 gar nichts aber wir prüfen dann eben danach immer wieder ob diese Variable nicht mittlerweile fünf ist natürlich sollte dieser Zugriff auf diese Variable 6:49 auch wieder durch ein pthread Mutex Lock geschützt werden sonst könnte es sein dass wir prüfen ob die Variable kleiner als fünf ist was in dem Moment wahr sein 6:58 könnte sodass wir dann in den Schleifenkörper gehen in dem Moment könnte die andere Funktion die countervariable aber auf fünf setzen 7:05 sodass wir uns dann in der countdownfunktion innerhalb der Schleife befinden obwohl die countervariable 5 ist was ja eigentlich nicht der Fall 7:13 sein darf diese Situation nennt man dann eine Race condition und die gilt es natürlich auch zu vermeiden nach der Prüfung in der Schleifenbedingung muss 7:22 aber natürlich der Mutex wieder unloggt werden damit die andere Funktion überhaupt die countervariable wieder hochzählen kann am Ende schleifenkörpers 7:30 muss dann aber der Mutex wieder gelockt werden weil als nächstes wieder die Schleifenbedingung geprüft wird wir also wieder auf die countervariable zugreifen 7:38 die Zeile mit dem pthread Mutex Lock genau vor der dekrementierung des Counters brauchen wir dann natürlich nicht mehr da wir wenn wir an dieser 7:45 Stelle angekommen sind ohnehin schon auf jeden Fall in dem geschützten Bereich sind eine Sache die wir noch machen sollten ist dass die vorschleife jetzt 7:53 nur noch ein Fünftel so oft durchlaufen sollte weil wir diese Schleife sonst niemals beenden würden später zeige ich euch noch wie wir das auch mit einer 8:00 Zeitbegrenzung anders lösen können wenn wir das Programm jetzt kompilieren und ausführen dann bekommen wir tatsächlich das richtige Ergebnis 8:08 der Counter steht am Ende wieder auf Null eine Sache ist aber dennoch nicht so gut und das hat mit dieser wildschleife zu tun diese läuft jetzt 8:16 gerade unbegrenzt oft und immer immer wieder durch die Funktion fragt also ständig ist die Variable jetzt noch kleiner als fünf was ist jetzt jetzt 8:23 immer noch und jetzt und jetzt und jetzt also immer wieder fragt sie ob die Variable immer noch kleiner als fünf ist für jede dieser Fragen benötigt der 8:30 Prozessor natürlich Rechenkapazität und das nennt sich dann aktives warten sinnvoller wäre es doch wenn das Programm sich schlafen legen könnte und 8:38 auf ein Signal wartet das ihm einfach signalisiert dass ich die Variable jetzt verändert hat lass uns mal schauen für wie viel Prozessorauslastung dieser 8:46 Programmaufruf jetzt gesorgt hat dazu modifiziere ich diesen timebefehl ein bisschen ich schreibe jetzt also hier command time - F und jetzt in 8:56 Anführungszeichen Zeit Doppel Prozente e das zeigt dir gleich die Zeit an und CPU Proz P zeigt die Prozessorauslastung an und dahinter dann noch den Namen des 9:07 Programms und das sieht schon nach einer recht hohen Auslastung aus um jetzt also die Auslastung zu reduzieren können wir die Funktion pthread Cont weight 9:17 Benutzen dieser übergeben wir eine Adresse einer Variable vom Typ pthread Cont t und eine Adresse zu einem Mutex diese pthread con t Variable ist 9:28 sozusagen ein Signal auf das gewartet wird wenn dies Signal noch nicht gesendet wurde dann hält das Programm hier an und erst wenn so ein Signal 9:36 verschickt wurde dann läuft dieser Thread an dieser Stelle weiter aber warum wird hier eigentlich noch eine Mutex Variable übergeben das hat den 9:43 Grund dass die Funktion pthread Cont weight nicht nur wartet sondern vorher auch noch dieses Mutex unlockt und nach dem Aufwachen auch wieder lockt und zwar 9:52 geschieht das unlocken und Einschlafen als eine atomare Operation sodass zwischen diesen beiden Schritten kein anderer threat in diesen kritischen 10:00 Bereich gelangen kann das gleiche gilt dann auch für das Aufwachen und locken auch das ist zusammen eine atomare Operation wir müssen hier jetzt also das 10:08 explizite Unlock und Log entfernen außerdem müssen wir eine solche pthread Conte variable natürlich erstmal anlegen z.B mit dem Namen Signal das mache ich 10:18 hier auch wieder im globalen Adressbereich weil später die countup Funktion auch auf diese gleiche Variable zugreifen soll damit diese dann das 10:25 Signal senden kann die Variable muss so wie auch die Mutex variable vorhin erstmal initialisiert werden und das mache ich natürlich auch in der 10:33 mainfunktion mit dem Befehl pthread Cont init und dort übergebe ich dann auch die Adresse auf diese condition variable und auch wieder als zweiten Parameter 0 so 10:44 die Countdown Funktion wartet jetzt auf dieses Signal also muss es auch eine Stelle im Code geben an der dieses Signal überhaupt gesendet wird und das 10:52 können wir hier in der countup Funktion machen z.B jedes Mal nachdem der Counter erhöht wurde dazu rufe ich die Funktion pthread Cont Signal auf und übergebe 11:01 natürlich auch wieder die Adresse dieser condition variable wenn wir jetzt das Programm wieder kompilieren und mit dem timebefehl starten dann sehen wir dass 11:09 die benötigte Zeit in etwa gleich ist aber die Prozessorauslastung stark gesunken ist eine letzte Sache die ich euch noch 11:17 zeigen möchte ist wie wir dieses warten mit einem Timer verbinden können es könnte ja sein dass wir gar nicht genau wissen wie oft wir diese Schleife 11:24 eigentlich in der Countdown Funktion ausführen müssen wenn wir diese z.B einfach in eine endlos wildschleife verwandeln denn dann endet unser 11:32 Programm ja eigentlich erstmal nie weil wir keine Abbruchbedingung mehr haben wir können jetzt aber die Funktion pthread Cont weight ersetzen durch 11:39 pthread Cont timed weight diese blockiert nicht für immer sondern bis wie vorher auch ein Signal eintrifft oder bis zu einem bestimmten Zeitpunkt 11:50 diesen Zeitpunkt können wir festlegen indem wir an diese Funktion eine Adresse auf eine Variable vom Typ struct timespack übergeben in diesem Struck 11:59 können Sekunden und Nanosekunden gespeichert werden und dazu muss man sagen die Zeitmessung auf Linux System bezieht sich immer auf die Sekunden die 12:07 seit dem 1 Januar 1970 vergangen sind und wir müssen jetzt angeben zu welchem Zeitpunkt die Funktion pthread Cont timed weight spätestens returnen soll 12:17 dazu können wir uns erstmal die aktuelle Zeit in diesem struct speichern und zwar mit der Funktion clock gettime daübergeben wir clock realtime und die 12:25 Adresse auf dieses timepack struct und wenn wir jetzt möchten dass der Timeout eine Sekunde später zuschlägt dann können wir einfach die Sekunden in 12:34 diesem struct um eins erhöhen also mit ts.tvs das sind die Sekunden da drin + GLE 1 die Adresse dieser Variable übergeben wir jetzt also an pthreadc 12:45 timetimed wait allerdings können wir momentan noch nicht entscheiden warum das Programm an dieser Stelle überhaupt weiter 12:52 ausgeführt werden kann wir wollen aber gerne registrieren wenn das durch einen Timeout zustande kam dazu können wir den Returnwert der Funktion nutzen den 13:01 können wir uns in einer Variable speichern und dann prüfen also z.B mit int Timeout g= pthread Cont timed weight und so weiter wenn dieser Wert jetzt 13:10 Null ist dann kam ein Signal an wenn dieser Wert aber e timed out ist das ist ein Makro das in rno.h definiert ist dann schlug der 13:20 Timeout zu wenn das der Fall ist dann wollen wir hier unser Programm beenden das mache ich jetzt an dieser Stelle mit pthread exit denn den damit kann man ja 13:29 auch einen Threat beenden um zu verdeutlichen dass ihr tatsächlich einen Timeout zuschlägt werde ich noch nach dem letzten hochzählen ein printbefehl 13:37 einbauen und auch direkt vor dem Beenden des Countdown Threads einfach damit man sieht dass an dieser Stelle eine Sekunde gewartet wird und dann eben dieser 13:45 Timeout zuschlägt wenn ich das Programm jetzt kompiliere und ausführe dann sieht man genau das und damit sind wir jetzt auch tatsächlich schon am Ende dieses 13:54 Videos oder bzw dieser drei Videos zum Thema Multithreading angekommen falls Fragen habt dann lasst es mich doch gerne in den Kommentaren wissen und bis 14:02 dahin viel Spaß beim coden [Musik]