Heisenbug?

Fragen zu Tkinter.
Sirius3
User
Beiträge: 17741
Registriert: Sonntag 21. Oktober 2012, 17:20

@Alfons Mittelmeyer: hör endlich auf, Zeug von einem bestimmten Solaris-System zu zitieren, das wahrscheinlich eine Userspace-Threadimplementierung umgesetzt hat, wo natürlich alle Threads kooperativ sein müssen, weil es keine höhere Instanz gibt, die einem Thread die Kontrolle nehmen kann. Linux, MacOS und Windows haben Threads anders implementiert. Da wird vom Betriebssystem in regelmäßigen Abständen ein Thread unterbrochen und die Kontrolle an einen anderen Thread übergeben. Wie das im Detail funktioniert, ist von System zu System unterschiedlich und braucht den normalen Nutzer nicht zu interessieren. Beim OP kommt Rechenzeitverschwendung mit aufwändigen GUI-Updates zusammen, was zu Problemen führt. Das Verwunderliche ist, wenn man die Rechenzeitverschwendung nur ein wenig einschränkt, bekommt die GUI das bißchen mehr an Rechenzeit, damit die gröbsten Probleme verschwinden. Deine ganzen Vorschläge sind aber von einer sauberen Lösung noch weit entfernt.

Zu Deiner letzten "Lösung": Du hast es geschafft, mit trigger_event globale Variablen zu benutzen, so dass Du wieder an Deinem Troll-Ziel angekommen bist. Sollte tkinter nach dem FIFO-Prinzip arbeiten werden erst alle Updates durchgeführt und dann das after mit trigger_event, dynamische FPS-Anpassung, wenn die GUI nicht mit zeichnen nachkommt. Dein Beschreibungstext behauptet das Gegenteil.

Das korrekte exakte Einhalten der Zeit (ganz ohne Event) erreichst Du so:

Code: Alles auswählen

    def hauptschleife(self):
        start = time()
        for frame in count():
            sleep(max(0, start + frame / FPS - time.time()))
            self.schritt()
            self.ausgabe()
@Üpsilon: wenn Du das Programm mit einem Update-Thread haben willst, ist eine Queue für Updates das falsche Mittel, weil, falls die GUI nicht mit dem Zeichnen hinterherkommt, nur der letzte Zustand relevant ist. Du brauchst also ein Spielfeld, das vom Update-Thread jeweils erzeugt und als ganzes an die GUI übergeben wird.
Benutzeravatar
kbr
User
Beiträge: 1487
Registriert: Mittwoch 15. Oktober 2008, 09:27

@Üpsilon: Vielleicht noch als Ergänzung: Thread-Programmierung ist nicht trivial, wenn man deadlocks und race-conditions vermeiden und auch mit shared-state fehlerfrei klarkommen will. Zudem ist das Debuggen von thread-basierten Programmen schwierig, um es mal euphemistisch auszudrücken.
Wie Sirius3 schon schrieb, ist die Art des Thread-Schedulings implementationsabhängig und wurde zuletzt auch mit Python 3.2 geändert. Da sollte man sich also nicht drauf verlassen, auch wenn ich mit sys.setswitchinterval eingegriffen hatte, damit der Consumer-Thread die Chance bekam, etwas länger laufen zu können. Das war aber nur zur Demonstration und ich würde dies in der Praxis nicht anwenden wollen. Die Doku zu setswitchinterval beschreibt auch, dass das Thread-Scheduling OS-spezifisch erfolgt.
Der saubere Weg ist es, nur so wenig wie möglich zu tun und es dem Scheduler dann wieder erlauben tätig zu werden. Und ansonsten in das Scheduling nicht weiter einzugreifen.
Bei Endlosschleifen ist sleep() dafür eine gute Wahl, wobei sleep(0) möglichst vermieden werden sollte. Den Effekt kannst Du mit einem Systemtool zur Darstellung der Prozessorlast gut verfolgen.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Sirius3 hat geschrieben: Zu Deiner letzten "Lösung": Du hast es geschafft, mit trigger_event globale Variablen zu benutzen, so dass Du wieder an Deinem Troll-Ziel angekommen bist. Sollte tkinter nach dem FIFO-Prinzip arbeiten werden erst alle Updates durchgeführt und dann das after mit trigger_event, dynamische FPS-Anpassung, wenn die GUI nicht mit zeichnen nachkommt. Dein Beschreibungstext behauptet das Gegenteil.
Sorry dass ich den Trigger nicht in Üpsilons Klasse eingebaut habe. Aber Du hast mit dem Verhalten recht, dass nämlich tkinter zuerst den update fertig macht und erst dann wieder das after zulässt
Sirius3 hat geschrieben: Das korrekte exakte Einhalten der Zeit (ganz ohne Event) erreichst Du so:

Code: Alles auswählen

    def hauptschleife(self):
        start = time()
        for frame in count():
            sleep(max(0, start + frame / FPS - time.time()))
            self.schritt()
            self.ausgabe()

Da hast Du auch recht, nur was soll die Funktion count? Die habe ich nirgends gefunden.

Ich habe es mal ausgetestet:

Code: Alles auswählen

    def hauptschleife(self):
        frame = 1
        start = time()
        zeit_vorher = start
        while True:
            if self.spiel_laeuft:
                sleep(max(0, start + frame / FPS - time()))
                zeit = time()
                print(int(10000*(zeit-zeit_vorher)))
                zeit_vorher = zeit
                self.schritt()
                self.ausgabe()
                frame += 1
            else:
                break

rr = Rapid_Roll()
Thread(target=rr.hauptschleife).start()
rr.screen.mainloop()

Dadurch, dass Python diesem Thread eine so hohe Priorität gibt, gibt es so gut wie keine Zeitschwankung. Ergebnis:

201
199
200
199
200
199
199
199
200
199
200
200
199
199
199
200
200
199
199
200
199
199
200
200
199
200
199
199
200
200
200
199
200
199
200
199
200
199
199
200
199


Der Thread ist die beste Lösung, da nur ein solcher wegen der ihm von Python eingeräumten sehr hohen Priorität ein konstantes Zeitverhalten ermöglicht.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Sirius3 hat geschrieben: Zu Deiner letzten "Lösung": Du hast es geschafft, mit trigger_event globale Variablen zu benutzen, so dass Du wieder an Deinem Troll-Ziel angekommen bist.
Dann ist aber auch klar, dass das rr auch weg muß. Und darüber hattest Du Dich noch nicht beschwert.

Code: Alles auswählen

#!/usr/bin/env python3
 
from canvas_screen import Screen
from random import randint, choice
from time import time
from threading import Lock, Thread, Event
from time import sleep
 
SPIELFELD_GROESSE = 500
BALKEN_AUF_EINMAL = 5
BALKEN_BREITE = 100
BALL_RADIUS = 20
SCROLL_GESCHWINDIGKEIT = 2 # Pixel pro Schritt
SEITWAERTS_GESCHWINDIGKEIT = 10
FPS = 50
 
class Rapid_Roll():
    def __init__(self):
        # linke Ecke der Balken als Koordinatentupel
        self.balken = [(randint(0, SPIELFELD_GROESSE-BALKEN_BREITE),
                        SPIELFELD_GROESSE/BALKEN_AUF_EINMAL*i)
                       for i in range(1,BALKEN_AUF_EINMAL+1)]
        # Mittelpunkt vom Ball
        self.ball = choice(self.balken)
        self.ball = [self.ball[0]+BALKEN_BREITE/2, self.ball[1]-BALL_RADIUS]
       
        self.gedrueckt = 0 # -1 für links, +1 für rechts
        self.gedrueckt_lock = Lock()
        self.spiel_laeuft = True
        self.screen = Screen(SPIELFELD_GROESSE, SPIELFELD_GROESSE)
        self.screen.window.bind('<Button-1>', lambda event: self.on_gedrueckt(-1))
        self.screen.window.bind('<Button-3>',  lambda event: self.on_gedrueckt(1))
        self.screen.window.bind('<ButtonRelease-1>', lambda event: self.on_gedrueckt(0))
        self.screen.window.bind('<ButtonRelease-3>', lambda event: self.on_gedrueckt(0))
        Thread(target=self.hauptschleife).start()
        self.screen.mainloop()

    def on_gedrueckt(self,value=None):
        self.gedrueckt_lock.acquire()
        if value != None:
            self.gedrueckt = value
        return_value = self.gedrueckt
        self.gedrueckt_lock.release()
        return return_value
 
    def schritt(self):
        # Ball seitwärts
        gedrueckt = self.on_gedrueckt()
        self.ball[0] += gedrueckt*SEITWAERTS_GESCHWINDIGKEIT
        if self.ball[0] < 0  or self.ball[0] > SPIELFELD_GROESSE:
            self.ball[0] -= gedrueckt*SEITWAERTS_GESCHWINDIGKEIT
        # Herausfinden, ob der Ball auf einem Balken liegt
        liegt_auf = any(b[0] <= self.ball[0] <= b[0]+BALKEN_BREITE and b[1]-BALL_RADIUS <= self.ball[1] <= b[1] for b in self.balken)
        # Ball hochziehen oder fallen lassen
        if liegt_auf:
            self.ball[1] -= SCROLL_GESCHWINDIGKEIT
        else:
            self.ball[1] += 2*SCROLL_GESCHWINDIGKEIT
        # Balken scrollen
        self.balken = [(x, y-SCROLL_GESCHWINDIGKEIT) for (x,y) in self.balken if y>SCROLL_GESCHWINDIGKEIT]
        while len(self.balken) <= BALKEN_AUF_EINMAL: # wenn ein Balken oben rausgerutscht ist
            self.balken.append((randint(0, SPIELFELD_GROESSE-BALKEN_BREITE),
                                self.balken[-1][1]+SPIELFELD_GROESSE/BALKEN_AUF_EINMAL))
        # prüfen ob der Ball noch im Feld ist
        if self.ball[1] < 0 or self.ball[1] > SPIELFELD_GROESSE:
           self.spiel_laeuft = False
 
    def ausgabe(self):
        for i, b in enumerate(self.balken):
            self.screen.input.put({'type':'line', 'name':i, 'bbox':(b[0], b[1], b[0]+BALKEN_BREITE, b[1])}),
        self.screen.input.put({'type':'circle', 'name':'ball', 'M':self.ball, 'r':BALL_RADIUS})
 
    def hauptschleife(self):
        start = time()
        frame = 1
        while True:
            if self.spiel_laeuft:
                sleep(max(0, start + frame / FPS - time()))
                self.schritt()
                self.ausgabe()
                frame += 1
            else:
                break

Rapid_Roll()
Und am Besten wäre es, wenn man das importiert:

Code: Alles auswählen

import rapid_roll
Bei diesem Script hat man dann ganz sicher keine globalen Variablen
Üpsilon
User
Beiträge: 222
Registriert: Samstag 15. September 2012, 19:23

Sirius3 hat geschrieben:@Üpsilon: was ist jetzt noch Dein Problem? Die Lösung ist, wie in meinem ersten Beitrag schon zu lesen, keinen eigenen Thread zu starten.
Ein Problem hab ich eigentlich keins mehr. Nur eine Frage, die in genau diesem Beitrag steht: viewtopic.php?f=18&t=40957#p312752 (den ich auf Seite 1 unten gepostet hatte und zwischendurch habe ich auch ein paar mal darauf verlinkt, aber offensichtlich hat sich niemand dazu herabgelassen, diesen Link anzuklicken).
Wieso jagt der Ball in dieser Variante auf undurchschaubare Weise rauf und runter, wenn man das print rausnimmt?

Mir ist durchaus klar, dass das Threading unnötig ist, aber ich wüsste doch ganz gern, woher dieser rätselhafte Fehler kommt!
PS: Die angebotene Summe ist beachtlich.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Alfons Mittelmeyer hat geschrieben:
Sirius3 hat geschrieben:Sollte tkinter nach dem FIFO-Prinzip arbeiten werden erst alle Updates durchgeführt und dann das after mit trigger_event, dynamische FPS-Anpassung, wenn die GUI nicht mit zeichnen nachkommt. Dein Beschreibungstext behauptet das Gegenteil.
Aber Du hast mit dem Verhalten recht, dass nämlich tkinter zuerst den update fertig macht und erst dann wieder das after zulässt
Da habe ich mich jetzt vertan. Richtig ist, dass das Triggern mit after mehr schwankt. Ist ja auch klar, wir haben ja noch das andere after, welches die Queue pollt und Canvas Items behandelt. Durch dieses zweite after wird natürlich das welches die 1/FPS triggern soll, beeinflußt, man könnte aber die beiden Takte synchonisieren, also mit einem after Takt beides behandeln. Da wäre dann auch das Verhalten interessant.

Ergebnis:

351
461
206
221
205
220
206
231
206
220
205
220
205
223
206
221
206
226
203
221
203
221
209
222
205
219
210
226
206
220
205
220
203
223
205
221
207
224
203
219
206
222
206
222
205
219
209
227
204
222
203
221
204
222
207
221
208
224
206
219
202
220
206
223
206
219
206
227
204
222
202
221
205
224
204
220
206
224
203
220
205
213
207
221
206
221
211
220
206
220

Der ersten beiden Werte sind hoch, also ist der Bildaufbau wohl mit dabei oder sonstige events beim Start. Und danach Schwankungen bis 2 ms ein Ausrutscher mit 3 ms war auch mit dabei. Genauigkeit darf man vom after timer nicht erwarten.

Oder sind es Thread Umschaltzeiten?. Das müßte man auch testen.
Zuletzt geändert von Alfons Mittelmeyer am Mittwoch 2. August 2017, 12:42, insgesamt 1-mal geändert.
Sirius3
User
Beiträge: 17741
Registriert: Sonntag 21. Oktober 2012, 17:20

@Alfons Mittelmeyer: __init__ einer Klasse soll das Objekt initialisieren und möglichst schnell zurückkehren. Durch einen Import verhindert man keine globalen Variablen, dass das Quatsch ist, hast Du hoffentlich aus dem Thread über globale Variablen von vor zwei Monaten gelernt.
Richtig ist die Verwendung einer main-Funktion

Code: Alles auswählen

def main():
    rr = Rapid_Roll()
    rr.screen.mainloop()

if __name__ == '__main__':
    main()
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Alfons Mittelmeyer hat geschrieben: Der ersten beiden Werte sind hoch, also ist der Bildaufbau wohl mit dabei oder sonstige events beim Start. Und danach Schwankungen bis 2 ms ein Ausrutscher mit 3 ms war auch mit dabei. Genauigkeit darf man vom after timer nicht erwarten.

Oder sind es Thread Umschaltzeiten?. Das müßte man auch testen.
Nein, Thread Umschaltzeiten sind es nicht, es ist der after timer. Jetzt war gar ein Ausrutscher mit 5 ms dabei. Das heißt, nur ein Thread gewährleistet genaue Zeiten. Nö, da war after am Ende statt am Anfang getriggert.

Meist ist auch after konstant:

203
205
205
203
205
204
203
205
204
203
204
205
203
203
205
202
204
205
203
205
205
204
204
205
204
205
204
202
224
203
204
203
203
204
203
204
204
204
204
205
203
203
203
204
206
206
206
231
208
206
205
206
205
205
204
203
204
205
204
204
205
204
203
204
203
204
206
204
204
208
204
204
207
203
203
205
203
203
205
203
203
204
203
203
205
203
207
204
202
204
204
203
206
204
203
203
213
203
203
203
202
203
204
202
203
204
202
204
204
203
204
204
204
204
204
202
204
204
204
203
205

Meist Werte 202, 204, 205 (Zehntel Millisekunden) aber dann wieder so etwas wie 224 oder 231 dazwischen. Konstanz also nur mit einem Thread höherer Priorität und sleep. Threadumschaltzeiten spielen keine Rolle, da schaut es genauso aus, wie hier direkt nach dem after.
Benutzeravatar
kbr
User
Beiträge: 1487
Registriert: Mittwoch 15. Oktober 2008, 09:27

Aus der tkinter-Doku zur after-method:
Requests Tkinter to call function callback with arguments args after a delay of at least delay_ms milliseconds. There is no upper limit to how long it will actually take, but your callback won't be called sooner than you request
Damit wäre eigentlich alles klar - aber wer Zweifel hat, mag es sicher noch einmal empirisch untermauern.

@Üpsilon: Deine Frage ist hier durchaus beantwortet worden.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Was IBM dazu schreibt:
SCHED_OTHER
This policy is defined by POSIX Standard 1003.4a as implementation-defined. The recalculation of the running thread's priority value at each clock interrupt means that a thread may lose control because its priority value has risen above that of another dispatchable thread.
Quelle: https://www.ibm.com/support/knowledgece ... hreads.htm

Gut finde ich dazu auch die Beschreibung von Oracle:
Timeshare Scheduling

Timeshare scheduling distributes the processing resource fairly among the LWPs in this scheduling class. Other parts of the kernel can monopolize the processor for short intervals without degrading response time as seen by the user.

The priocntl(2) call sets the nice(2) level of one or more processes. The priocntl() call also affects the nice() level of all the timesharing class LWPs in the process. The nice() level ranges from 0 to +20 normally and from -20 to +20 for processes with superuser privilege. The lower the value, the higher the priority.

The dispatch priority of time shared LWPs is calculated from the instantaneous CPU use rate of the LWP and from its nice() level. The nice() level indicates the relative priority of the LWPs to the timeshare scheduler.

LWPs with a greater nice() value get a smaller, but nonzero, share of the total processing. An LWP that has received a larger amount of processing is given lower priority than one that has received little or no processing.
Quelle: https://docs.oracle.com/cd/E19455-01/80 ... index.html

Ja es ist Time Sharing. Doch durch Setzten des nice values kann das Programm, das heißt Python die Priorität festlegen. Von der Endlosschleife her müßte der Nicht GUI Thread eine niedrige Priorität begommen, damit auch der GUI Thread wieder dran kommt. Doch wenn Python für den GUI Thread den nice value bis zum Limit hochgesetzt hat, weil da kein Python Code auszuführen ist, dann kommt es so ziemlich auf dasselbe hinaus, wie bereits vorher geschrieben.

Das stimmt nicht, das der GUI Thread gar keine Rechenzeit bekommt, er bekommt lediglich fast gar keine Rechenzeit.

Events und auch after werden natürlich behandelt, weil das Events sind, aber der Bildaufbau bekommt fast keine Rechenzeit!
Zuletzt geändert von Alfons Mittelmeyer am Mittwoch 2. August 2017, 13:59, insgesamt 2-mal geändert.
BlackJack

@Alfons Mittelmeyer: Was ist denn bitte der GUI-Thread in dem kein Python-Code auszuführen ist und wo wird denn von Python der nice-Wert gesetzt? Ersteres gibts nicht und zweiteres wird nicht gemacht.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

BlackJack hat geschrieben:@Alfons Mittelmeyer: Was ist denn bitte der GUI-Thread in dem kein Python-Code auszuführen ist und wo wird denn von Python der nice-Wert gesetzt? Ersteres gibts nicht und zweiteres wird nicht gemacht.
Es geht nicht darum, dass da kein Python Code drin steht, sondern dass er nicht zur Ausführung ansteht. Die GUI wurde aufgebaut. Ein Event steht nicht zur Ausführung an. Tkinter ist in seiner mainloop und da im sleep Modus. Also kein Python Code steht zur Ausführung an. Erst dann, wenn ein after getriggert wird. Und das wird auch ausgeführt, weil das ein Event ist. Aber wenn der sleep der Mainloop endet und tkinter den Bildaufbau machen will, dann bekommt tkinter nicht die Rechenzeit dafür.

Weiß auch nicht, was es sonst sein kann. Evtl. Probleme mit dem GIL. Es soll da ja viele Probleme mit Multicore CPUs geben. Etwa eine CPU behandelt Threads mit IO und versucht den GIL von der anderen CPU zu bekommen. Code zur Ausführung 3 ticks. Versuche den GIL zu bekommen 16000 ticks.

Siehe http://www.dabeaz.com/python/GIL.pdf
Seite 37 ff

Es könnte natürlich auch sein, dass da, weil es kein Python Code ist, gar nicht versucht wird den GIL zu bekommen und damit auch den Thread, sofern der andere Thread den nicht selber abgibt. Aber das wird wohl nur sehr schwer herauszufinden sein.

Vielleicht hat jemand ja ein Python ohne GIL?
Antworten