Heisenbug?

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

Üpsilon hat geschrieben:Leute, so hatte ich das Pythonforum aber nicht in Erinnerung, dass hier Threads für schwer verständliche Diskussionen über Technikalitäten gehijacket werden. Mögt ihr vielleicht den ganzen Kreppes abtrennen und auf meine Frage viewtopic.php?f=18&t=40957#p312752 eingehen?
Also entweder Du setzt ein sleep(0) in Deine pollschleife rein, auch wenn das keiner wahrhaben will, oder Du nimmst das. Das bezieht sich aber auf Deinen Eingangspost und ich habe auch das seitwärts korrigiert. Allerdings einen anderen Thread hast Du dann nicht mehr, obwohl die Locks noch drin sind:

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
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))
 

    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):
        zeit = time()
        while self.spiel_laeuft:
            while time()-zeit < 1/FPS:
                sleep(0)
                #pass
            zeit += 1/FPS
            self.schritt()
            self.ausgabe()
    '''

    def hauptschleife(self):
        if self.spiel_laeuft:
            self.screen.window.after(int(1000/FPS),self.hauptschleife)
        self.schritt()
        self.ausgabe()

rr = Rapid_Roll()
##Thread(target=rr.hauptschleife).start()
rr.hauptschleife()
rr.screen.mainloop()
Und die Beantwortung der Frage: wenn man kein sleep reinmacht oder eine IO Operation wie print kommt der GUI Thread nicht mehr zum Bildaufbau dran, weil dem Python Interpreter Nicht Python Code egal ist und er nicht auf den GUI Thread umschaltet, damit der den Bildaufbau auch noch erledigen kann.

Also, mit IO Operationen ist es so, wenn Du etwa ein input einfügst, dann wartet der Thread nicht etwa, bis Du Deine Eingabe gemacht hast, sondern das Betriebssystem schaltet dann sofort auf einen anderen Thread um. Erst Nach Drücken von Enter kommt dann der Thread wieder dran. Bei Print und anderen IO Operationen ist es ähnlich. Das Betriebssystem schaltet richtig um und berücksichtigt auch den GUI Thread. Python mit Endlosschleife tut das aber nicht.

Python schaltet nach einer Maximalzeit dann den Thread um aber nur, wenn es in einem anderen Thread auch auf Python Ebene etwas zu tun gibt. Und das ist dann im GUI Thread aber nicht der Fall.
Sirius3
User
Beiträge: 17737
Registriert: Sonntag 21. Oktober 2012, 17:20

@Üpsilon: was ist jetzt noch Dein Problem? Die Lösung ist, wie in meinem ersten Beitrag schon zu lesen, keinen eigenen Thread zu starten. Wenn Du wissen willst, warum Deine urpsrüngliche Lösung nicht funktioniert: Python erlaubt nur einen gleichzeitig laufenden Thread. Dein busy-loop scheint aus welchen Gründen auch immer fast die gesamte Rechenzeit an sich zu reißen. Warum, ist eigentlich egal, weil der Fehler der Busy-Loop ist, den man immer durch **ein** sleep ersetzen kann.

Die restlichen Beiträge sind zum einen Teil (A.M.) reine Spekulation, bzw. versuche, das klar zu stellen. Das Niveau sind aber reine Implementierungsdetails, die morgen schon wieder anders sein könnten, also für einfache Programme nicht relevant.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

__deets__ hat geschrieben:Man I muss das schon richtig lesen: auch SCHED_OTHER kennt ein quantum. Und "fair progress among all..."..
The thread to run is chosen from the static priority 0 list based on
a dynamic priority that is determined only inside this list. The
dynamic priority is based on the nice value (see below) and is
increased for each time quantum the thread is ready to run, but
denied to run by the scheduler. This ensures fair progress among all
SCHED_OTHER threads.
Gelesen hast Du es ja vielleicht richtig, verstanden hast Du es offensichtlich nicht, Es gibt eine dynamische Priorität - die man auch als nice value bezeichnet. Und jedes Zeit Quantum, wo der Thread bereit wäre zum Starten - er also in der Queue der startbereiten Threads steht, er aber dann vom Scheduler doch nicht gestartet wird - das kann z.B. sein, dass ein anderer Tread noch vor ihm dran ist, oder dass ein Event für einen anderen Thread getriggert wird, dannn wird seine Priorität erhöht - nice value Wert.

Und was bedeutet das? Wenn ein anderer Thread blockiert - etwa auch durch sleep, dann hat er, wenn er lange warten mußte, eine höhere Priorität und kommt dann eher dran, wie andere, die nicht solange warten mußten. Und das ist der faire Prozess.

Das hat mit dem Zeitquantum wie beim SCHED_RR Thread, bei dem der Thread nach einem maximalen Zeit Quantum unterbrochen wird, überhaupt nichts zu tun.

Wenn man Quantum liest und wieder Quantum liest, dann kann das auch ein ganz anderes Quantum sein, wie etwa ein Quantum Bier zuviel.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Sirius3 hat geschrieben:Dein busy-loop scheint aus welchen Gründen auch immer fast die gesamte Rechenzeit an sich zu reißen. Warum, ist eigentlich egal, weil der Fehler der Busy-Loop ist, den man immer durch **ein** sleep ersetzen kann.

Die restlichen Beiträge sind zum einen Teil (A.M.) reine Spekulation, bzw. versuche, das klar zu stellen. Das Niveau sind aber reine Implementierungsdetails, die morgen schon wieder anders sein könnten, also für einfache Programme nicht relevant.
Das sind nicht welche Gründe auch immer. Der sleep ist völlig richtig. und Spekulationen von mir sind das schon gar nicht, denn das sind die Fakten:
schedpolicy SCHED_OTHER
New thread uses Solaris-defined fixed priority
scheduling; threads run until preempted by a higher-priority thread or until they block or yield.
Es ist fast die ganze Rechenzeit, Nur durch das Pollen mit after kommt der GUI Thread dran und schafft es nebenher nach langer Zeit auch noch einen Bildaufbau zu machen, was man aber vergessen kann. Ohne sleep kommt nicht das Betriebssystem dran, welches den Taskwechsel vollzieht, bei dem tkinter den Bildaufbau machen kann. Besser als sleep ist aber keine eigene Task sondern ein after, denn sonst erfolgt während dem Bildaufbau kein Taskwechsel, und dann sind eventuiell die 1/FPS nicht einzuhalten. Ein after würde ein Event auslösen, das Vorrang vor Fertigstellung des Bildaufbaus hat.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

@Üpsilon: wenn es aber unbedingt doch ein anderer Thread sein soll, kann man vom GUI Thread aus auch den Übergang in den anderen Thread forcieren, wenn man nämlich die 1/FPS einhalten will und nicht warten will, bis tkinter ganz mit dem Bildaufbau fertig ist.

Diesen Übergang kann man nämlich durch triggern mit after und event.set() auslösen:

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))
        self.event = Event() 

    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):
        while True:
            self.event.wait()
            self.event.clear()
            if self.spiel_laeuft:
                self.schritt()
                self.ausgabe()
            else:
                break

def trigger_event():
   rr.screen.window.after(int(1000/FPS),trigger_event)
   rr.event.set()

rr = Rapid_Roll()
Thread(target=rr.hauptschleife).start()
trigger_event()
rr.screen.mainloop()
Sirius3
User
Beiträge: 17737
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: 17737
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