Auswertung Profile: lange Methode 'acquire' of 'thread.lock'

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
derhendrik
User
Beiträge: 17
Registriert: Donnerstag 15. Januar 2015, 16:39

Hallo liebe Python-Gemeinde!

Leider weiß ich nicht genau, zu welchem Unterforum meine Frage am besten passt, deswegen poste ich sie hier. Ich denke auch, dass sie unabhängig von PyQt bzw. dem Raspberry Pi ist. Hauptsächlich geht es um eine plötzlich auf dem Raspi auftauchende Methode 'Acquire' of 'thread.lock', welche den Großteil der Rechenzeit beansprucht.

Kurz etwas zu meinem Hintergrund: Derzeit versuche ich mich an einer GUI, welche ich mit PyQt entwerfe. Um diese möglichst sauber zu programmieren verwende ich auch mehrere Threads: in einem werden zyklisch Sensorwerte von einem Arduino erfasst, der zweite beinhaltet die eigentliche GUI. Da es komfortabler ist, schreibe ich das Programm auf meinem Laptop, schlussendlich soll es jedoch auf einem Raspberry Pi 2 laufen. Auf meinem Laptop läuft es auch sauber, auf dem Raspberry Pi 2 ist es aber unbrauchbar langsam. Um hier der Ursache auf den Grund zu gehen, habe ich mal den cProfiler über die main-Funktion laufen lassen.

Ich habe dann einfach mal die Anwendung für circa eine Minute laufen lassen, nur um mal zu sehen, inwieweit sich die erstellten Profile von Laptop und Raspi unterscheiden. In dem Raspi-Profil wird knapp die Hälfte der Zeit von eben dieser Methode 'Acquire' of 'thread.lock' belegt! Ich dachte erst, mein Programm läuft so langsam, weil dem Raspi die notwendige Power fürs Zeichnen der Plots fehlt, dies ist aber alles im grünen Bereich. Die Methode 'Acquire' of 'thread.lock' kommt in meinem Laptop-Programm auch gar nicht vor.

Ich hänge einfach mal 2 Screenshots, sowie die jeweiligen Profile an diesen Post an.
Laptop: Bild
Raspi: Bild

Ich hoffe irgendjemand kann mir hier weiterhelfen bzw. mich in die richtige Richtung weisen. Leider kenne ich niemanden, der mir bei so einem spezifischen Problem helfen könnte.

Klar ausformuliert habe ich somit folgende Fragen:
1) Was genau ist dieses "method 'acquire' of 'thread.lock'"? (dazu habe ich mich schon in der Pythondokumentation versucht schlauzumachen)
2) Warum taucht es auf einmal auf dem Raspberry Pi auf, auf dem Laptop hingegen gar nicht?
3) Was müssten meine nächsten Schritte sein, um dieses Problem zu beheben?

Ich kann euch auch gerne noch den Quellcode anhängen, leider müssen dazu relativ viele Module importiert werden.

Danke für eure Hilfe!
Hendrik
derhendrik
User
Beiträge: 17
Registriert: Donnerstag 15. Januar 2015, 16:39

BlackJack

@derhendrik: `aquire()` wartet bis das Lock gesperrt werden kann und tut es dann. Ich vermute mal Du verwendest einen ”alten” Raspi mit nur einem Prozessorkern aber einen Laptop mit mehreren Kernen. Auf dem Raspi kann immer nur ein Thread auf einmal den Prozessor benutzen, alle anderen müssen warten. Und wenn die ein `aquire()` machen dann halt solange bis dieser eine Thread der das Lock gerade besitzt, es wieder freigibt. Selbst wenn sie auf ein Lock warten das ein anderer Thread gerade besitzt: *der* kann es ja auch erst wieder freigeben wenn er mal den Prozessor hat.

Beim Laptop können mehrere Sachen wirklich *gleichzeitig* passieren, beim (alten) Raspi, wie bei allen Einprozessor-Systemen mit nur einem Prozessorkern eben alles immer hübsch der Reihe nach.
derhendrik
User
Beiträge: 17
Registriert: Donnerstag 15. Januar 2015, 16:39

Hallo BlackJack!
Erst einmal wieder "Danke" für deine Hilfe, du hattest mir ja bei meinen PyQt-Fragen schon so kompetent weitergeholfen.
Eine Vermutung in die Richtung hatte ich bereits auch, aber das Problem ist, dass ich wirklich den neuen Raspberry Pi 2 Model B verwende.
Außerdem verwende ich das PyQt-eigene Threading, falls das relevant sein sollte.

Was mich außerdem wundert ist, dass es eben 'thread.lock' heißt, ich hätte erwartet, dass es etwas mit 'threading' statt 'thread' sein müsste...

Wenn es hart auf hart kommt, versuche ich einfach mal die ganze Anwendung ohne einen eigenen Thread für das zyklische Einlesen der Sensorwerte zu programmieren. Wollte es halt besonders "richtig" machen, sodass die Anwendung immer "responsive" bleibt, auch wenn gerade mit PySerial eingelesen wird!
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Anscheinend greifst Du in einem Thread auf etwas zu, was die anderen auch brauchen, brauchst aber dort sehr lange und läßt die andern Schlange stehen.

Müßte man eben wissen, was Dein Programm zwischen lock.acquire() und lock.release() treibt. Doch hoffentlich wohl keine Schleife, oder? Zyklisch klingt irgendwie danach.

Die Frage wäre da, machst Du diesen lock.aquire oder macht PyQt das aus irgendwelchen Gründen selber?

Vielleicht zeigst Du mal diesen Thread. Wenn er lang ist, bitte dann die Details rauslöschen. Struktur mit Kommentar würde reichen.

Ach so, wenn dieses aquire nicht von Dir kommt, dann wird es schwierig sein, herauszufinden wodurch. Ich würde auf das zyklische Einlesen tippen, dass da evtl etwas nicht stimmt.
derhendrik
User
Beiträge: 17
Registriert: Donnerstag 15. Januar 2015, 16:39

Hallo Alfons!
Auch dir Danke für deine Hilfe.
Alles was mit lock.acquire() und lock.release() geschieht habe ich nicht selbst implementiert.

Bin selber noch sehr unerfahren was das Programmieren umfangreicherer Anwendungen angeht. Deshalb habe ich hier einfach auf das PyQt-eigene Threading zurückgegriffen. Um die notwendigen Messwerte zwischen Einlese-Thread und Visualisierungs-Thread auszutauschen, benutze ich diesen PyQt-Signal-Slot-Mechanismus, da dieser "threadsicher" ist.

Ich habe sicherheitshalber nochmal gerade die zwei Profiles miteinander verglichen:

Im Laptop-Profil: weder lock noch release zu finden
Im Raspi-Profil: beide, sowohl lock als auch release zu finden. release braucht für 4765 calls nur 0.01 Sekunden. lock braucht für 9527 calls 31.46 Sekunden...

Um deine Frage mit der Schleife zu beantworten: Für die zyklische Abfrage nutze ich keine "while True:"-Schleife per se, aber sowas in der Art: Dazu verwende ich einen Timer (auch von Qt), welcher alle 100ms erneut die Funktion "read_values" aufruft. In dieser "read_values" scheint ja auch das Problem zu liegen, in den Bildern habe ich diese Funktion mit dem Cursor ausgewählt, sodass man die Zeit sieht (vgl. 12.080s vs. 39.739s)

Aber genug davon, hier der Thread:

Code: Alles auswählen

class ArduinoThread(QtCore.QThread):
    
    def __init__(self):
        super(ArduinoThread, self).__init__()
    
        # Hier wird der Timer mit der 100ms-Refresh-Rate erstellt
        self.updatetimer = QtCore.QTimer()
        self.updatetimer.setInterval(100)
        self.updatetimer.setSingleShot(False)
        self.updatetimer.start()

    def run(self):        
        port_name = 'COM3'
        baud_rate = '9600'
        
        self.fuellhoehe = 0
        self.BA = 0
        self.energie_l = 0
        self.energie_s = 0
        self.energie_h = 0
        self.counter_input_flush = 0
        
        # Verbindung zum Arduino herstellen
        try:
            self.arduino = serial.Serial(port=port_name,baudrate=baud_rate,timeout = None)
            time.sleep(1)
            self.arduino.flushInput()
            self.updatetimer.timeout.connect(self.read_values)
        except SerialException:
            print "Arduino-Port konnte nicht geoeffnet werden."         
        
    # Hier die Funktion für das zyklische Einlesen der Messwerte, welche anscheinend die Probleme verursacht    
    def read_values(self):        
        try:
            value_string = self.arduino.readline()
        except SerialException:
            print ("Verbindung verloren")
            value_string = 'ERROR'
        print ("Eingelesener String:")        
        print value_string

        #Hier wird der empfangene String einfach ein bisschen weiter verarbeitet um die eigentlichen Werte zu extrahieren
        value_dictionary = dict(i.split(':') for i in value_string.split(';'))
        value_dictionary['energie_h']=value_dictionary['energie_h'].rstrip('\r\n')
        
        self.fuellhoehe = float(value_dictionary['fuellhoehe'])
        self.BA = int(value_dictionary['BA'])
        self.energie_l = float(value_dictionary['energie_l'])
        self.energie_s = float(value_dictionary['energie_s'])
        self.energie_h = float(value_dictionary['energie_h'])

        # Hier wird mit dem Signal "update_arduino" die Aktualisierung der GUI im anderen Thread ausgelöst. Dazu werden die neuen Messwerte übergeben
        self.emit(QtCore.SIGNAL("update_arduino"),self.fuellhoehe,self.BA,self.energie_l,self.energie_s,self.energie_h)
        
        #Damit man sie auch in der Console sieht
        print self.fuellhoehe
        print self.BA
        print self.energie_l
        print self.energie_s
        print self.energie_h
        

        # alle 100 Messwerte wird der InputBuffer der seriellen Schnittstelle geleert um einen "offset" zu vermeiden. Evtl. Überflüssig.
        self.counter_input_flush += 1
              
        if self.counter_input_flush == 100:
            self.arduino.flushInput()
            time.sleep(0.02)
            self.counter_input_flush = 0
            print "*******************FLUSH***********************"            

    
    # Eine Sendefunktion, wenn über die GUI die Betriebsart (deshalb "BA") des Arduinos geändert werden soll
    def send_BA(self,BA):
        self.arduino.writelines(BA)


PS: Leider habe ich den Raspberry Pi zurzeit nicht hier. Gerne versuche ich aber nochmal ein Mini-Programm zu schreiben, in welchem das Problem weiterhin auftritt, wozu aber nicht die ganze zusätzlichen Module und Hardware (PySerial, Stylesheet von der Anwendung, Arduino, etc...) gebraucht werden, sodass ihr das bei Interesse auch testen könnt.

PPS: Ich muss glaube ich nochmal ein bisschen selber an der ganzen Geschichte rumdoktoren. Ein paar Stellschrauben dir mir Einfallen:
1) Multithreading sein lassen und mit einem Thread probieren
2) Das emittieren des Signals "update_arduino" auskommentieren. Zwar aktualisiert dann nicht die GUI, aber ich kann ja in der Console sehen, ob die Messwerte "aktuell" sind
3) Hat zwar nix mit Threading zu tun, aber: evtl anderes Modul zum lesen der seriellen Schnittstelle (PyTTY statt PySerial)
4) Hat zwar nix mit Threading zu tun, aber: evtl gucken ob es nicht schlauer wäre nicht mit "readline" sondern "read" zu arbeiten, da das Zeilenende von Linux und Windows glaube ich unterschiedlich ist (also Carriage return + Linefeed statt nur Linefeed. Müsste ich aber nachschauen)


Ich glaube auch, dass das erstellte Profile mit cProfile nur so mittelgut für Multithread-Anwendungen geeignet ist. Vielleicht muss ich da auch nochmal gucken, ob es nicht eine bessere Lösung gibt.

Gerade habe ich auch diesen Beitrag hier gefunden: https://groups.google.com/forum/#!msg/p ... jLLbnZpA4J
"avoid threading whenever possible" hieß es dort.
Vielleicht sollte ich diesen Ratschlag wirklich beherzigen. Stellschrauben hier wären z.B. nicht so oft Messwerte einlesen (wenn dies der Engpass ist) oder nicht so oft die GUI zu aktualisieren (wenn dies denn der Engpass ist).

Hoffe auf weitere Inputs! Euch einen schönen Samstag!
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

@derhendrik Ich sehe das Problem, dass es hier mit der Zeit sehr knapp werden könnte mit einem Timer Intervall von 100 ms. Statt diesem Timer nimm am Besten

Code: Alles auswählen

void QTimer::singleShot(int msec, const QObject * receiver, const char * member)
Rufe zu Beginn read auf, direkt oder über diesen Timer und am Ende von read rufe wieder read über diesen timer auf. Dann sollte es keine Zeitprobleme geben.

Oder kommt es Dir darauf an, dass wirklich alle 100 ms das geschieht? Besser zuerst die Funktion ausführen und dann timer, wann sie das nächstemal aufgerufen werden soll.

Ob es daran liegt, weiß ich nicht, aber wäre eine durchaus mögliche Ursache.

Sorry, für Python:

Code: Alles auswählen

QTimer.singleShot (int msec, callable receiver)
derhendrik
User
Beiträge: 17
Registriert: Donnerstag 15. Januar 2015, 16:39

Oh okay! Verstehe was du meinst. Habe es mal so ähnlich in Tkinter mit "after" gemacht, eben dass die Funktion sich selber wieder aufruft. Das ging eigentlich ganz gut!

Danke für eure Inputs, habe ja jetzt einige Möglichkeiten mit denen ich arbeiten kann.

War nämlich etwas überrascht und enttäuscht, dass dem Raspberry Pi 2 bei so einer trivialen Aufgabe schon die Puste ausging. Also wird es wohl eher am Code liegen.

Ich werde die nächsten Tage mal weiterbasteln und dann berichten!

Grüße

Hendrik
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

@derhendrik Und denke an die Zeit. Wenn nämlich der Raspberry langsamer ist als derr Laptop und Du für read schon an die 100 ms brauchst, anscheinend manchmal auch mehr - daher der lock - hat das übrige System keine Zeit zum Arbeiten mehr. (Deine Timeraufrufe geraten nämlich in einen Lock, weil der vorige Aufruf von read noch nicht fertig ist). Und wenn Du mit Deinem Programm noch etwas anderes machen willst, als dieses Auslesen und Anzeigen, dann ist dafür keine Zeit mehr übrig.

Wenn ein System viele Sachen tut, wäre es sinnvoll die Logik zentral zu verwalten. Etwa Auslesen auf Anforderung hin. Wenn das System nicht mehr mit etwas anderem beschäftigt ist und dann Zeit zum Auslesen hat, ja dann kann es das tun. Und direkt GUI Funktionen sollte man dann auch nicht aufrufen, sondern lediglich die Daten übergeben, damit die GUI sie abholen kann, wenn Zeit dafür ist. Ich mache das etwa mit einem Message System. Alles kommt in eine Queue und wird dann der Reihe nach abgearbeitet, bis die Queue leer ist. Und da die Aktionen durch die Schnelligkeit der Abarbeitung bestimmt sind, kann sich da auch nichts aufstauen. Aber wenn andere Tasks timergesteuert immer mehr anstauen, kommt es zum Problem, dass dann etwas nicht mehr nachkommt. Timer verwenden ja, aber danach, wenn nichts mehr zu tun ist, damit das System nicht einschläft.

Also Timer so verwenden, dass sich Pausen ergeben, aber nicht so, dass dem System dann die Puste ausgeht.
derhendrik
User
Beiträge: 17
Registriert: Donnerstag 15. Januar 2015, 16:39

Ahhhhh okay, danke Alfons!

Das war mir gar nicht so klar. Ich dachte, dass 100ms massig Zeit wären um diese paar Zeilen Code auszuführen. Vielleicht liegt es ja wirklich an dem Einlesen der seriellen Schnittstelle, ich habe schon einige Forenbeiträge zu "how to speed up PySerial??" gefunden. Im schlussendlichen Programm werden auch die ganzen print-statements entfernt, weiß aber nicht inwieweit das hilfreich ist.

Bin schon gespannt, ob ich das noch sauber zum Laufen kriege!
Euch nochmal "Danke" und ich werde berichten!
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

derhendrik hat geschrieben:Ahhhhh okay, danke Alfons!

Das war mir gar nicht so klar. Ich dachte, dass 100ms massig Zeit wären um diese paar Zeilen Code auszuführen. Vielleicht liegt es ja wirklich an dem Einlesen der seriellen Schnittstelle, ich habe schon einige Forenbeiträge zu "how to speed up PySerial??" gefunden. Im schlussendlichen Programm werden auch die ganzen print-statements entfernt, weiß aber nicht inwieweit das hilfreich ist.

Bin schon gespannt, ob ich das noch sauber zum Laufen kriege!
Euch nochmal "Danke" und ich werde berichten!
Die serielle Schnittstelle sollte normalerweise nicht so langsam sein. Aber bei dem Timerintervall von 100 ms musst Du ja alles mitberücksichtigen, was sonst noch im System abläuft und das könnte insgesamt knapp werden.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

@hendrik Hab nochmal nachgedacht. Das Problem ist nicht die Schnelligkeit der seriellen Schnittstelle. Und wenige Zeilen können lange dauern. Was hältst Du etwa von dieser

Code: Alles auswählen

:a = input()
Und so etwas hast Du nämlich mit readline(). Wenn Du zu langsam liest, ist es nichts, weil sich da der Buffer aufstaut und Du nicht die aktuellsten Werte bekommst. Wenn Du schnell liest, hängst Du, aber das macht nichts.
Am besten wäre hier tatsächlich eine direkte Endlosschleife, wie beim Input. Also nicht timergesteuert. Und danach kannst Du Dich entscheiden, ob Du das schon weiter auswerten und an GUI schicken willst.

Ich würde sagen, lass das ganz mit dem Timer und ruf read einfach in einer Endlosschleife auf.
Und professionell ist: GUI update nicht wenn Meßwerte kommen, sondern wenn sie sich geändert haben.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

@derhendrik: Problem gelöst? Ich dachte Du wolltest berichten?
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@derhendrik:
Wenn Lesen und Schreiben am Arduino mit QTimer schnell genug sind, brauchst Du keinen separaten Thread, dann kannst Du alles ereignisgesteuert im Hauptthread implementieren (multithreading für Arme). So wie Du den Timer initialisiert, läuft auch alles im Hauptthread, die Klasse ArduinoThread ist komplett unnütz.

Ein QTimer ist in Qt zwingend an seinen Threadkontext gebunden (siehe Doku dazu). Da Du diesen in `QThread.__init__` initialisierst, bleibt er im Hauptthread. Beachte - nur der Code unter `QThread.run` ist innerhalb eines neuen Threads, d.h. Du müsstest den Timer dort initialisieren, damit er im Subthread läuft. Das ist übrigens ein häufig gemachter Fehler im Umgang mit `QThread`, weshalb die Qt-Leute von der Benutzung der Klasse `QThread` abraten. Die Threadaffinität gilt für alle QObjects, d.h. aufgerufene Methoden laufen im Threadkontext des Objektes:

Code: Alles auswählen

from threading import currentThread
from PyQt4.QtGui import QApplication
from PyQt4.QtCore import QThread, QTimer

class MyThread(QThread):
   
    def __init__(self):
        QThread.__init__(self)
        print 'MyThread.__init__', currentThread(), QThread.currentThreadId()


    def run(self):
        def read_run():
            print 'read_run', currentThread(), QThread.currentThreadId()

        print 'MyThread.run', currentThread(), QThread.currentThreadId()
        timer = QTimer()
        timer.setInterval(100)
        timer.setSingleShot(False)
        timer.start()
        timer.timeout.connect(read)
        timer.timeout.connect(read_run)
        timer.timeout.connect(self.read)
        self.exec_()

    def read(self):
        print 'MyThread.read', currentThread(), QThread.currentThreadId()

def read():
    print 'read', currentThread(), QThread.currentThreadId()

print currentThread(), QThread.currentThreadId()
app = QApplication([])
thread = MyThread()
thread.start()
app.exec_()
Das Objekt `thread` ist im Hauptthread, daher wird auch `thread.read` dort ausgeführt. Das ist zunächst verwirrend, da man denkt, `QThread` wäre eine Klasse für einen Subthread. Da ist es eben nicht, es ist eher eine Verwaltungsobjekt für Subthreads (in `.run`) und lebt selbst im Kontext des Elternthreads.
derhendrik
User
Beiträge: 17
Registriert: Donnerstag 15. Januar 2015, 16:39

Hallo alle Mitleser!

Das Problem mit dem Threading habe ich jetzt auch bemerkt, jerch.
Gestern habe ich mich gewundert, warum ich den Timer eigentlich schon in der __init__ starte. Nachdem ich ihn in der "run" starten wollte, kam die Fehlermeldung, dass ich einen Timer nicht aus einem anderen Thread starten darf, da bin ich erstmal stutzig geworden.
Habe es dann auch so gemacht wie von dir vorgeschlagen und mir die "currentThread()" und "currentThreadId" ausgeben lassen und zum ersten mal gemerkt, dass init und run nicht im selben Thread laufen.

Daraufhin habe ich einfach versucht den ganzen Timer (also Erstellen des Timers und das Starten) in die "run" zu verlegen, da hat aber erstmal gar nix mehr geklappt.
Gerade habe ich überprüft, wo die read_values Funktion abläuft und auch da hast du recht: im main-Thread.

Ich glaube die größte Baustelle ist, dass ich mich mal intensiver mit der Dokumentation befassen muss. Das klappt mittlerweile schon "ganz okay", zumindest weiß ich dann wie meine Google-Anfrage auszusehen hat. Zum Beispiel, dass für einen QTimer in einem workerthread eine EventLoop benötigt. (bitte entschuldigt das denglish)

Danke für eure Hilfe! Ich glaube, ich "fange nochmal klein an" und versuche die Anwendung sauber neu zu schreiben, erstmal ohne das Multithreading bzw. lese mich in das eventhandling ein.

Eigentlich war ich erstmal ganz zufrieden damit, wie der Code auf meinem PC gelaufen ist, aber da war ich wohl etwas blauäugig :D

Grüße

Hendrik
derhendrik
User
Beiträge: 17
Registriert: Donnerstag 15. Januar 2015, 16:39

Hier noch ein kleiner Nachtrag:
Danke für eure Hilfe, dadurch ist mir Vieles klarer geworden. Eben auch so Sachen wie dass ich aus Versehen noch im main-Thread bin, da mein Timer in der init des Worker-Threads erstellt wurde etc.

Habe es jetzt hinbekommen, dass es ausreichend flüssig läuft.
Die Lesefunktion läuft jetzt wie gewünscht im Workerthread.
Den Timer habe ich jetzt als "Singleshot" in der Lesefunktion implementiert, welcher beim Auslösen die Lesefunktion selber aufruft.

Zusätzlich habe ich alle "print"-statements rausgenommen. Diese haben, wie ich feststellen musste, alles sehr verlangsamt. Ich habe ja alle 100ms verschiedene Listen, Messwerte, "wo bin ich im code", etc ausgeben lassen.

Darüber hinaus plotte ich jetzt nicht alle 100ms neu. Ich habe nämlich feststellen müssen, dass sich dadurch Knöpfe, Tabwechsel etc nicht mehr bedienen ließen. Es wurde zwar geplottet, der Rest fror aber einfach ein.
Hier muss ich aber nochmal nachforschen, ob das nicht trotzdem schneller geht.
Zurzeit nutze ich pyqtgraph: Hier wird ein Benchmark mitgeliefert, welches auf dem Raspberry Pi für einen einzelnen Plot mit 30fps läuft.
Das ist aber nochmal ein ganz anderes Thema.

Zum Abschluss habe ich aber noch eine ganz andere Frage an euch:
Ich hatte mir erhofft, dass durch meinen Multithreading-Versuch alles schneller läuft und ich mir die Möglichkeit der "Gleichzeitigkeit" zunutze mache. Wird so ein Workerthread, oder generell ein Thread in Python automatisch auf einem anderen CPU-Kern als der Hauptthread laufen? Bringt Multithreading überhaupt den gewünschten Geschwindigkeitszuwachs, falls alles nur auf einem Kern läuft? (Ich glaube, dass Multithreading eben keine MulticoreCPU vorraussetzt)

Bei dem Raspberry Pi sieht man es ja immer ganz schön, wenn Anwendungen nicht von den mehreren Kernen Gebrauch machen: Ich habe beispielsweise das pyqtgraph Benchmark gestartet, die CPU Auslastung lag dann bei 25%, was ja zu erwarten war. Ich habe dann insgesamt 4 mal das Benchmark gestartet, die CPU-Auslastung lag dann um die 100% und alle sind mit ihren 30fps gelaufen. Da liefen dann aber ja alle Sachen getrennt in ihrem eigenen Interpreter (???) und nicht als Threads.

Schönen Abend euch!

Hendrik
BlackJack

@derhendrik: Die Frage(n) sind nicht so einfach zu beantworten. Welcher Thread auf welcher CPU oder auf welchem Kern läuft ist erst einmal Sache des Betriebssystems, das kann nach verschiedenen Kriterien entscheiden welcher Prozess und welcher Thread innerhalb eines Prozesses auf welcher CPU läuft. Das ist an sich auch gut so denn nur das Betriebssystem hat ja den Gesamtüberblick den ein einzelner Prozess nicht hat. Also was so alles in anderen Prozessen abgeht, wer gerade welche Ressource benutzt und so weiter.

Bei CPython kommt noch hinzu das immer nur ein Thread zur gleichen Zeit Python-Bytecode ausführen kann wegen dem „global interpreter lock“ (GIL). Wirklich parallel dazu können in anderen Threads Codeteile in C-Erweiterungen laufen die das GIL nicht brauchen. Also beispielsweise Operationen auf Numpy-Arrays oder auch Sachen bei Qt die ”länger” dauern so dass es sich lohnt das GIL freizugeben. Wenn Du eine 100%-Auslastung mit mehreren Graphen bei einem Mehrkernprozessor bekommst, dann läuft da das meiste tatsächlich parallel. Bei einem Einkern-System bekommt man natürlich keinen Geschwindigkeitsschub durch Threads. Wie sollte das den auch gehen‽ Es gibt halt nur den einen Kern und der kann immer nur einen Thread in einem Prozess zur gleichen Zeit ausführen.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@derhendrik:
Wenn es bei einem Timerevent alle 100ms ruckelt oder die GUI zeitweise einfriert, brauchen Deine Aktionen im Hauptthread zulange (länger als die 100ms). Falls Du noch viele Berechnungen im Worker in Python machst, addiert sich das des GILs wegen hinzu. Um die FPS hoch zu kriegen, wäre dann vllt. ein Blick auf das multiprocessing-Modul lohnenswert. Zuvor würde ich aber mittels eines Profilers schauen, was da eigentlich die Rechenzeit beansprucht.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

derhendrik hat geschrieben:Habe es jetzt hinbekommen, dass es ausreichend flüssig läuft.
Die Lesefunktion läuft jetzt wie gewünscht im Workerthread.
Den Timer habe ich jetzt als "Singleshot" in der Lesefunktion implementiert, welcher beim Auslösen die Lesefunktion selber aufruft.
Es ist fraglich, ob überhaupt ein Timer erforderlich ist. Denn wie sollte ein modernes Betriebssystem etwa bei input funktionieren:

Wenn ein input da ist und es wurde noch keine Zeile eingegeben, dann blockiert das Systen. Allerdings bei einem modernen Betriebssystem sieht dann das so aus, dass der Thread schlafen geht und ein anderer Thread dran kommt. Erst wsenn der User eine Zeile eingegeben hat, erwacht der Thread wieder.

Bei readline aber bin ich mir nicht sicher. Wenn man einen File etwa als eine Arte pipe benutzt und eine Anwendung schreibt hinein und eine andere liest jeweils eine Zeile heraus, dann kehrt sie auch mit 0 Daten zurück, wenn noch gar nichts geschrieben wurde. In so einem Falle ist ein Timer zu empfehlen.

Wenn es allerdings bei einer seriellen Schnittstelle wie bei einem input ist, macht ein Timer, wie Du ihn vorher hattest, überhaupt keinen Sinn, denn dann gibt es einen Stau beim Input Threadlock.
Bei input ist die Endlosschleife zu empfehlen , weil dort ja beim input sowieso gewartet wird und man keinen zusätzlichen Timer braucht. Allerdings ein Timer mit 0 Wartezeit danach wäre dasselbe wie eine Endlosschleife.

Wenigstens jetzt, wo Du den Timer danach erst setzt, knallen keine Timerfunktionen mehr auf den readline Threadlock

Du solltest testen, wie das mit readline bei der seriellen Schnittstelle ist. Wenn da readline auch wartet, bis eine Zeile eintrifft, dann brauchst Du keinen Timer und kannst eine Endlosschleife machen oder aber Du setzt die Wartezeit Deines Timers auf 0.

Und schneller würde es gehen, wenn Du ein GUI Update erst machst, wenn sich die Daten geändert haben, sich also von den vorherigen unterscheiden.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@derhendrik:
Wie sieht denn der Output des Arduino über die Zeit aus? Ohne timeout blockiert der Subthread in der Tat mit `readline` bis ein EOL an der seriellen Schnittstelle gefunden wurde.
Antworten