Heisenbug?

Fragen zu Tkinter.
Üpsilon
User
Beiträge: 222
Registriert: Samstag 15. September 2012, 19:23

(Vorsicht, viel Code :D )
Hallo miteinander,

ich wollte mal schlau sein und habe versucht, eine kleine Hilfsklasse zu schreiben, um das Programmieren mit Tkinter angenehmer zu machen und anschließend im Akkord Grafikspielchen zu fabrizieren. Das hab ich mir so gedacht, dass Objekte dieser Klasse ein Queue als Attribut halten, in das man (ggf. aus einem anderen Thread heraus) Zeichenanweisungen im Gewand eines Dictionaries reinschmeißen kann, und eine parallel laufende Methode kümmert sich ums Zeichnen. Bis jetzt sind die Fähigkeiten sehr rudimentär, aber es funktioniert schon einigermaßen.

canvas_screen.py

Code: Alles auswählen

#!/usr/bin/env python3

import tkinter as tk
from time import sleep
from queue import Queue, Empty

class Screen:
    def __init__(self, width, height):
        self.window = tk.Tk()
        self.canvas = tk.Canvas(self.window, width=width, height=height)
        self.name2id = dict()
        self.input = Queue()
        self.canvas.pack()

    def mainloop(self):
        self.process_loop()
        self.window.mainloop()

    def process_loop(self):
        while True:
            try:
                self.process_element(self.input.get_nowait())
            except Empty:
                break
        self.window.after(1, self.process_loop)

    def process_element(self, e):
        if e['name'] in self.name2id:
            # Update
            if e['type'] == 'circle':
                self.canvas.coords(self.name2id[e['name']], e['M'][0]-e['r'], e['M'][1]-e['r'], e['M'][0]+e['r'], e['M'][1]+e['r'])
            elif e['type'] == 'text':
                self.canvas.coords(self.name2id[e['name']], e['M'])
                self.configure(self.name2id[e['name']], text=e['text'], font=e['font'])
            elif e['type'] == 'line':
                self.canvas.coords(self.name2id[e['name']], *e['bbox'])
            # Delete
            if e['type'] == 'delete':
                self.canvas.delete(self.name2id[e['name']])
        else:
            # Create
            if e['type'] == 'circle':
                self.name2id[e['name']] = self.canvas.create_oval(e['M'][0]-e['r'], e['M'][1]-e['r'], e['M'][0]+e['r'], e['M'][1]+e['r'])
            elif e['type'] == 'text':
                self.name2id[e['name']] = self.canvas.create_text(e['M'][0], e['M'][1], text=e['text'], font=e['font'])
            elif e['type'] == 'line':
                self.name2id[e['name']] = self.canvas.create_line(*e['bbox'])

                
if __name__ == '__main__':
    s=Screen(500, 500)
    s.process_element({'name':'horst', 'type':'circle', 'M':(100,400), 'r':40})
    s.process_element({'name':'hallo', 'type':'text', 'M':(200,200), 'text':'hallo', 'font':('Courier New', 40)})
    s.process_element({'name':'lydia', 'type':'line', 'bbox':(100,100,500,300)})
    s.window.update()
    sleep(1)
    s.input.put({'name':'horst', 'type':'circle', 'M':(400,400), 'r':40})
    s.input.put({'name':'hallo', 'type':'delete'})
    print(s.name2id)
    s.mainloop()
 
Jetzt zum kniffligen Teil. Habe versucht, das alte Nokia-Handyspiel "Rapid Roll" nachzubauen, bei dem es darum geht, dass man einen Ball seitwärts steuern kann, wobei überall Balken sind, die sich nach oben bewegen, und wenn der Ball auf einem Balken liegt, bewegt er sich logischerweise ebenfalls nach oben, und wenn nicht, fällt er runter, und man muss den Ball möglichst lang innerhalb der Begrenzungen des Spielfeldes halten. Hier der Code:

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 Thread, Lock

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('<1>', self.on_L)
        self.screen.window.bind('<3>', self.on_R)
        self.screen.window.bind('<ButtonRelease-1>', self.on_nichts)
        self.screen.window.bind('<ButtonRelease-3>', self.on_nichts)

    def on_L(self, e):
        self.gedrueckt_lock.acquire()
        self.gedrueckt = -1
        self.gedrueckt_lock.release()
    def on_R(self, e):
        self.gedrueckt_lock.acquire()
        self.gedrueckt = +1
        self.gedrueckt_lock.release()
    def on_nichts(self, e):
        self.gedrueckt_lock.acquire()
        self.gedrueckt = 0
        self.gedrueckt_lock.release()

    def schritt(self):
        # Ball seitwärts
        self.gedrueckt_lock.acquire()
        self.ball[0] += self.gedrueckt*SEITWAERTS_GESCHWINDIGKEIT
        if 0 < self.ball[0] < SPIELFELD_GROESSE:
            self.ball[0] -= self.gedrueckt*SEITWAERTS_GESCHWINDIGKEIT
        self.gedrueckt_lock.release()
        # 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)
        print("x")
        # 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 not 0 < 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:
                pass
            zeit += 1/FPS
            self.schritt()
            self.ausgabe()

rr = Rapid_Roll()
Thread(target=rr.hauptschleife).start()
rr.screen.mainloop()
 
So funktioniert erstmal alles, außer natürlich dem Prüfen ob das Spiel vorbei ist, weil der entsprechende Abschnitt auskommentiert ist, und (seltsamerweise) funktioniert die Seitwärts-Steuerung auch nicht. Aber der Ball bewegt sich so auf und ab, wie er soll.

Aber jetzt kommt der Hammer: Wenn ich das print in Zeile 57 auskommentiere, funktioniert gar nichts mehr. Man sieht dann nur ein leeres, hängendes Konsolenfenster und es öffnet sich KEIN Tkinter-Fenster. Habe keine Ahnung, wieso :( Wahrscheinlich hab ich irgendwas mit den Threads verkackt, aber ich sehe nicht, was genau. Wer kann helfen?

Danke im Voraus und freundliche Grüße!
PS: Die angebotene Summe ist beachtlich.
Üpsilon
User
Beiträge: 222
Registriert: Samstag 15. September 2012, 19:23

Ok, in Zeile 52 fehlt ein not. Damit wäre das mit der Steuerung geklärt. Bleibt nur noch die Frage wieso es ohne das print nicht geht.
PS: Die angebotene Summe ist beachtlich.
Üpsilon
User
Beiträge: 222
Registriert: Samstag 15. September 2012, 19:23

Hallo, habe diesen Bug jetzt weiterentwickelt: Dass es nicht startete, hing daran, dass die Queue viel zu schnell vollgeballert wurde, die Zeichen-Schleife also dauerhaft lief und es somit nie zum Mainloop-Aufruf in Zeile 17 des ersten Moduls kam. Habe es jetzt so eingerichtet, dass mindestens alle so-und-soviel Zeichenbefehle das Fenster geupdatet wird.

Dafür gibts jetzt aber nen neuen rätselhaften Fehler, der ebenfalls verschwindet, wenn man ein print einbaut. Code und Details kommen morgen ... aber es scheint hier ja eh keinen zu interessieren.
PS: Die angebotene Summe ist beachtlich.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

@Üpsilon: hab nur mal einen kurzen Blick auf Dein Programm geworfen. Und da sind mir zwei Dinge aufgefallen, die man bestimmt nicht tun sollte.

Das eine war:
sleep(1)

Was für einen Sinn soll das ergeben, wenn Du das Programm eine Sekunde lang schlafen legst und es in dieser Zeit nichts tun kann?

Das andere ist Deine Hauptschleife:

Code: Alles auswählen

    def hauptschleife(self):
        zeit = time()
        while self.spiel_laeuft:
            while time()-zeit < 1/FPS:
                pass
            zeit += 1/FPS
            self.schritt()
            self.ausgabe()
Schleifen sind immer schlecht, weil sie blockieren und CPU Zeit vergeuden. Da gibt es doch sicherlich auch einen Timer, oder? Lass doch vielleicht auch einmal in der Zwischenzeit die GUI ran, oder was denkst Du?
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Die andere Frage wäre dann auch, wenn Du statt Deiner Schleife eine mit Timer nimmst, warum Du dann überhaupt einen extra Thread brauchst und nicht gleich after nimmst und dann auch die Queue nicht brauchst.
Sirius3
User
Beiträge: 17712
Registriert: Sonntag 21. Oktober 2012, 17:20

@Üpsilon: Alfons hat es ja schon geschrieben: durch Deinen Busy-Loop im Thread bleibt keine Zeit mehr für das Hauptprogramm. Durch das »print« übernimmt das Betriebssystem für kurze Zeit die Kontrolle, so dass für das Hauptprogramm auch ein bißchen davon abfällt.

Dein Programm wird durch den Thread und die Queue unnötig kompliziert.

Code: Alles auswählen

#!/usr/bin/env python3
 
import tkinter as tk
from random import randint, choice

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 Screen(tk.Tk):
    def __init__(self, width, height):
        super().__init__()
        self.name2id = dict()
        self.canvas = tk.Canvas(self, width=width, height=height)
        self.canvas.pack()
 
    def mainloop(self):
        self.process_loop()
        super().mainloop()
 
    def process_loop(self):
        self.after(1000//FPS, self.process_loop)
        self.process()
        
    def process(self):
        pass

    def move_circle(self, name, position, radius):
        bbox = position[0]-radius, position[1]-radius, position[0]+radius, position[1]+radius
        if name not in self.name2id:
            self.name2id[name] = self.canvas.create_oval(*bbox)
        else:
            self.canvas.coords(self.name2id[name], *bbox)

    def move_line(self, name, points):
        if name not in self.name2id:
            self.name2id[name] = self.canvas.create_line(*points)
        else:
            self.canvas.coords(self.name2id[name], *points)
 
class RapidRoll(Screen):
    def __init__(self):
        super().__init__(SPIELFELD_GROESSE, SPIELFELD_GROESSE)
        # 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
        ball_position = choice(self.balken)
        self.ball = [ball_position[0]+BALKEN_BREITE/2, ball_position[1]-BALL_RADIUS]
       
        self.gedrueckt = 0 # -1 für links, +1 für rechts
        self.bind('<KeyPress-Left>', self.on_L)
        self.bind('<KeyPress-Right>', self.on_R)
        self.bind('<KeyRelease-Left>', self.on_nichts)
        self.bind('<KeyRelease-Right>', self.on_nichts)
 
    def on_L(self, e):
        self.gedrueckt = -1
    def on_R(self, e):
        self.gedrueckt = +1
    def on_nichts(self, e):
        self.gedrueckt = 0
 
    def schritt(self):
        # Ball seitwärts
        self.ball[0] += self.gedrueckt*SEITWAERTS_GESCHWINDIGKEIT
        if not 0 < self.ball[0] < SPIELFELD_GROESSE:
            self.ball[0] -= self.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 not 0 < self.ball[1]< SPIELFELD_GROESSE:
           self.spiel_laeuft = False"""
 
    def ausgabe(self):
        for i, b in enumerate(self.balken):
            self.move_line(i, (b[0], b[1], b[0]+BALKEN_BREITE, b[1]))
        self.move_circle('ball', self.ball, BALL_RADIUS)
 
    def process(self):
        self.schritt()
        self.ausgabe()
 
rr = RapidRoll()
rr.mainloop()
Üpsilon
User
Beiträge: 222
Registriert: Samstag 15. September 2012, 19:23

Ok danke. Aber so richtig verstehe ich das noch nicht. Die Busy-Loop läuft doch in nem anderen Thread. Wieso verhindert die denn, dass die Gui im Hauptthread rankommt? Die Threads laufen doch "gleichzeitig" oder etwa nicht?
PS: Die angebotene Summe ist beachtlich.
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

Nein, tun sie in Python nicht. Das verhindert das global Interpreter lock. Und selbst wenn sie das täten belastet ein solcher spinning thread das System unnötig & verzögert mindestens mal andere Aufgaben.
Üpsilon
User
Beiträge: 222
Registriert: Samstag 15. September 2012, 19:23

Ok sorry, habe mich missverständlich ausgedrückt. Also: ich habe mal gelernt, dass mehrere scheinbar gleichzeitig laufende Threads in Wirklichkeit immer abwechselnd laufen, erst der eine für eine ganz kurze Zeit, dann der andere für eine ganz kurze Zeit usw. Wieso läuft dann der Thread mit der Busy-Loop also lange und lässt den Hauptthread nicht ran?
PS: Die angebotene Summe ist beachtlich.
BlackJack

@Üpsilon: Er lässt ihn ja ran aber der kommt in der Zeit in der er dran ist halt nicht dazu die Queue abzuarbeiten bevor dann der andere Thread dran ist und noch mehr in die Queue steckt. Dadurch kommen zwar beide dran, nur aus der ``while``-Schleife kommt der GUI-Thread nie heraus, und damit friert die GUI ein. Das ist unabhängig davon, dass in CPython immer nur ein Thread gleichzeitig Python-Bytecode abarbeiten kann. Und es könnte sogar sein, dass der C-Code von Tk/Tcl tatsächlich parallel zum Python-Bytecode im anderen Thread läuft.

Was da tatsächlich parallel laufen kann und was nicht ist auch ein Implementierungsdetail auf das man sich nicht verlassen kann/sollte, denn andere Python-Implementierungen müssen das „global interpreter lock“ (GIL) ja nicht haben. Auch zukünftige Versionen von CPython nicht. Jython beispielsweise hat kein GIL und PyPy möchte es gerne loswerden.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Üpsilon hat geschrieben:Ok danke. Aber so richtig verstehe ich das noch nicht. Die Busy-Loop läuft doch in nem anderen Thread. Wieso verhindert die denn, dass die Gui im Hauptthread rankommt? Die Threads laufen doch "gleichzeitig" oder etwa nicht?
Also, es ist so. Es ist davon auszugehen, dass serselbe Prozessor, welchen den GUI Thread bearbeitet, auch den anderen Thread bearbeitet. Und so wird von einem Thread in den anderen gewechselt. Die Threads kommen also hintereinander dran und laufen nicht gleichzeitig. Ein Threadwechsel findet statt, wenn es in dem augenblicklichen Thread nichts mehr zu tun gibt, oder wenn eine Maximalzeit abgelaufen ist.

In Deiner Schleife pollst Du fast sinnlos, da die GUI nicht dran ist, während Du pollst. Sie kommt dann erst wieder dran, wenn die Maximalzeit abgelaufen ist. Wenn dann nach em GUI Thread der andere Thread wieder dran kommt, hat die GUI eventuell Werte hinterlegt, die Du dann beim ersten Poll bekommst und dann bekommst Du lange wieder nichts mehr.

Der print Befehl dürfte zumindest unter Windows einen vorzeitigen Wechsel in den Main Thread also in den GUI Thread auslösen, sodass Du dann eher etwas bekommst.

Pollen ist grundsätzlich verkehrt, man macht es nur bei Tkinter, weil es dort kein threadsicheres Event gibt. Statt after(1,..) wäre after (10,..) besser, um tkinter mehr Freiraum zu lassen. 1/100 Sekunde sollte wohl immer noch genügend schnell sein.

GUI in einem extra Thread zu bearbeiten macht an sich keinerlei Sinn. Wenn Du es trotzdem tun willst, dann wäre auch noch eine Queue für den anderen Thread angebracht. Die pollt man aber nicht, sondern macht einen loop mit event:

Code: Alles auswählen

        self.event = threading.Event()
        self.event.set()
        self.__looping = True
        while self.__looping:
            self.event.wait()
            self.event.clear()
            self.do_work() # Queue bearbeiten
Das wäre so eine Schleife für den anderen Thread. Und die GUI legt dann etwas auf die Queue des anderen Threads und ruft dann sein self.event.set() auf.

Das self.event.wait() sorgt dafür, dass die Schleife nicht läuft, wenn nichts da ist und dann der GUI Thread dran ist.
BlackJack

@Alfons Mittelmeyer: Deine Ausführungen zu Prozessoren und Threads sind nicht richtig.  Weder für CPython, noch allgemein.

Und auch das mit dem `Event` macht keinen Sinn. Das löst kein hier bestehendes Problem. Und die doppelten Unterstriche gehören da nicht hin.
Üpsilon
User
Beiträge: 222
Registriert: Samstag 15. September 2012, 19:23

Prozessoren und Events hin oder her, ich habs nun geschnallt. Aufs Forum ist eben Verlass :) Danke

Sirius' Variante funktioniert prima. Ich weiß gar nicht mehr, wieso ich mich so sehr aufs Threading versteift hab, das macht hier wahrscheinlich mehr Probleme als es nützt. Vielleicht sollte ich überdenken, ob meine Hilfsklasse überhaupt einen Mehrwert hat ...

Spaßeshalber wüsste ich aber gern noch eine Sache:
Ähnlicher Code, neuer Bug:

Code: Alles auswählen

#!/usr/bin/env python3

import tkinter as tk
from time import sleep
from queue import Queue, Empty

class Screen:
    def __init__(self, width, height, max_changes_per_frame):
        self.window = tk.Tk()
        self.canvas = tk.Canvas(self.window, width=width, height=height)
        self.max_changes_per_frame = max_changes_per_frame
        self.name2id = dict()
        self.input = Queue()
        self.canvas.pack()

    def mainloop(self):
        changes = 0
        while True:
            try:
                self.process_element(self.input.get_nowait())
            except Empty:
                self.window.update()
            else:
                changes += 1
                if changes == self.max_changes_per_frame:
                    self.window.update()
                    changes = 0

    def process_element(self, e):
        if e['name'] in self.name2id:
            # Update
            if e['type'] == 'circle':
                self.canvas.coords(self.name2id[e['name']], e['M'][0]-e['r'], e['M'][1]-e['r'], e['M'][0]+e['r'], e['M'][1]+e['r'])
            elif e['type'] == 'text':
                self.canvas.coords(self.name2id[e['name']], e['M'])
                self.configure(self.name2id[e['name']], text=e['text'], font=e['font'])
            elif e['type'] == 'line':
                self.canvas.coords(self.name2id[e['name']], *e['bbox'])
            # Delete
            elif e['type'] == 'delete':
                self.canvas.delete(self.name2id[e['name']])
                del self.name2id[e['name']]
            else:
                raise ValueError(e)
        else:
            # Create
            if e['type'] == 'circle':
                self.name2id[e['name']] = self.canvas.create_oval(e['M'][0]-e['r'], e['M'][1]-e['r'], e['M'][0]+e['r'], e['M'][1]+e['r'])
            elif e['type'] == 'text':
                self.name2id[e['name']] = self.canvas.create_text(e['M'][0], e['M'][1], text=e['text'], font=e['font'])
            elif e['type'] == 'line':
                self.name2id[e['name']] = self.canvas.create_line(*e['bbox'])
            else:
                raise ValueError(e)

                
if __name__ == '__main__':
    s=Screen(500, 500, 3)
    s.process_element({'name':'horst', 'type':'circle', 'M':(100,400), 'r':40})
    s.process_element({'name':'hallo', 'type':'text', 'M':(200,200), 'text':'hallo', 'font':('Courier New', 40)})
    s.process_element({'name':'lydia', 'type':'line', 'bbox':(100,100,500,300)})
    s.window.update()
    sleep(1)
    s.input.put({'name':'horst', 'type':'circle', 'M':(400,400), 'r':40})
    s.input.put({'name':'hallo', 'type':'delete'})
    print(s.name2id)
    s.mainloop()

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 Thread, Lock

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.spiel_laeuft = True
        self.screen = Screen(SPIELFELD_GROESSE, SPIELFELD_GROESSE, 6)
        # Steuerung mit der Maus
        self.gedrueckt = 0 # -1 für links, +1 für rechts
        self.steuerung_lock = Lock()
        self.screen.window.bind('<1>', self.on_L)
        self.screen.window.bind('<3>', self.on_R)
        self.screen.window.bind('<ButtonRelease-1>', self.on_L_los)
        self.screen.window.bind('<ButtonRelease-3>', self.on_R_los)

    def on_L(self, e):
        self.steuerung_lock.acquire()
        self.gedrueckt = -1
        self.steuerung_lock.release()
    def on_R(self, e):
        self.steuerung_lock.acquire()
        self.gedrueckt = +1
        self.steuerung_lock.release()
    def on_L_los(self, e):
        self.steuerung_lock.acquire()
        if not self.gedrueckt == +1: # falls die andere Taste schon gedrückt wurde, bevor die eine losgelassen wird
            self.gedrueckt = 0
        self.steuerung_lock.release()
    def on_R_los(self, e):
        self.steuerung_lock.acquire()
        if not self.gedrueckt == -1:
            self.gedrueckt = 0
        self.steuerung_lock.release()

    def schritt(self):
        # Ball seitwärts
        self.steuerung_lock.acquire()
        self.ball[0] += self.gedrueckt*SEITWAERTS_GESCHWINDIGKEIT
        if not 0 < self.ball[0] < SPIELFELD_GROESSE:
            self.ball[0] -= self.gedrueckt*SEITWAERTS_GESCHWINDIGKEIT
        self.steuerung_lock.release()
        # 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 not 0 < 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:
            print("x")
            while time()-zeit < 1/FPS:
                pass
            zeit += 1/FPS
            self.schritt()
            self.ausgabe()

rr = Rapid_Roll()
Thread(target=rr.hauptschleife).start()
rr.screen.mainloop()
Ohne das print in Zeile 86 düst der Ball auf eine für mich undurchschaubare Weise hoch und runter. Mit dem print funktioniert es aber. Weiß jemand, wieso?

Lg Y.
PS: Die angebotene Summe ist beachtlich.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

BlackJack hat geschrieben:@Alfons Mittelmeyer: Deine Ausführungen zu Prozessoren und Threads sind nicht richtig.  Weder für CPython, noch allgemein.

Und auch das mit dem `Event` macht keinen Sinn. Das löst kein hier bestehendes Problem. Und die doppelten Unterstriche gehören da nicht hin.
Meine Ausführungen sind haargenau richtig. Mit den doppelten Unterstrichen hast Du allerdings recht. Das ist auch ein Code Ausschnitt, den ich vor zwei Jahren geschrieben hatte, also noch in meiner Anfängerphase. Ja es sollte nur ein Unterstrich sein.

Wahrscheinlich handelt es sich beim verwendeten Betriebssystem nicht um ein Echtzeitbetriebssystem, bei dem man Threads unterschiedlichen Prozessoren zuweisen kann. Und in Python kann man das sowieso nicht. Also kommt Zeitscheibentechnik zum Einsatz. Und wie das funktioniert, habe ich beschrieben. Und das mit dem print habe ich auch erklärt. Und meine Lösung löst genau das Problem.
BlackJack

@Alfons Mittelmeyer: Deine Ausführungen sind nicht richtig. Du beschreibst da wie Threads vielleicht funktionieren könnten, aber so absolut als wenn das im vorliegenden Fall tatsächlich so wäre. Es ist aber eher unwahrscheinlich das diese Beschreibung auf die Systeme die Leute heute so verwenden tatsächlich zutrifft. Auf mein Debian-System auf einem Mehrkern-x64-Prozessor jedenfalls nicht, und ich vermute stark das Windows auf dem gleichen Rechner da keinen Unterschied macht. Dazu kommt dann auch das „global interpreter lock“ (GIL) das man bei einer Beschreibung auch noch berücksichtigen müsste.

Letztendlich ist das alles aber für das vorliegende Problem auch völlig irrelevant, denn man hätte das gleiche Problem wenn beide Threads, selbst ohne GIL, auf zwei verschiedenen Prozessoren laufen würden: Wenn der Producer-Thread mehr in die Queue schiebt als der Consumer-Thread verarbeiten kann, hat man ein Problem — egal wie die Threads implementiert sind.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

BlackJack hat geschrieben:@Alfons Mittelmeyer: Deine Ausführungen sind nicht richtig. Du beschreibst da wie Threads vielleicht funktionieren könnten, aber so absolut als wenn das im vorliegenden Fall tatsächlich so wäre.
Ja es gibt Mehrkernprozessoren. Bei meinen Linux Betriebsystemen übernimmt die Graikaufbereitung bestimmt ein anderer Kern, denn da ist alles flüssig. Bei meinem Windows7 32 Bit ruckeln aber die Widgets, wenn ich sie mit der Maus ziehe. Evtl. übernimmt da nicht einmal die Grafikausgabe ein anderer Kern. Ob aber einen anderen Python Thread ein anderer Kern übernimmt, ist zu bezweifeln. Und wie kann man bestimmen, welchen Thread welcher Kern übernimmt, wenn etwas gleichzeitig sein soll? Wo ist die Gewähr dafür daß es nicht derselbe Kern ist? Schließlich laufen viele Prozesse und Threads auf einem System und wenn man die Threads nicht bestimmten Kernen zusweisen kann, gibt es keinerlei Gewähr dafür.
BlackJack hat geschrieben: Letztendlich ist das alles aber für das vorliegende Problem auch völlig irrelevant, denn man hätte das gleiche Problem wenn beide Threads, selbst ohne GIL, auf zwei verschiedenen Prozessoren laufen würden: Wenn der Producer-Thread mehr in die Queue schiebt als der Consumer-Thread verarbeiten kann, hat man ein Problem — egal wie die Threads implementiert sind.
Wenn die Threads richtig implementiert sind, nämlich ohne pollen, existiert das Problem nicht. Hierfür sollte man eine saubere Lösung implementieren. Den anderen Thread geht die GUI nichts an und bind Befehle haben im anderen Thread nichts zu suchen.

Die GUI soll die Werte an den anderen Thread übergeben. Das ist mal das Erste:

Code: Alles auswählen

#!/usr/bin/env python3
 
import tkinter as tk
from time import sleep
from queue import Queue, Empty
 
class Screen(tk.Tk):
    def __init__(self, width, height):
        tk.Tk.__init__(self)
        self.canvas = tk.Canvas(self, width=width, height=height)
        self.name2id = dict()
        self.input = Queue()
        self.canvas.pack()
        self.extern_trigger = self.noop

    def noop(self,value):
        pass

    def mainloop(self,extern_trigger=None):

        if extern_trigger:
            self.extern_trigger = extern_trigger

        self.bind('<1>', self.on_L)
        self.bind('<3>', self.on_R)
        self.bind('<ButtonRelease-1>', self.on_nichts)
        self.bind('<ButtonRelease-3>', self.on_nichts)

        self.process_loop()
        tk.Tk.mainloop(self)

    def on_L(self, e):
        self.extern_trigger(-1)
    def on_R(self, e):
        self.extern_trigger(1)
    def on_nichts(self, e):
        self.extern_trigger(0)

    def process_loop(self):
        while True:
            try:
                self.process_element(self.input.get_nowait())
            except Empty:
                break
        self.after(10, self.process_loop)
 
    def process_element(self, e):
        if e['name'] in self.name2id:
            # Update
            if e['type'] == 'circle':
                self.canvas.coords(self.name2id[e['name']], e['M'][0]-e['r'], e['M'][1]-e['r'], e['M'][0]+e['r'], e['M'][1]+e['r'])
            elif e['type'] == 'text':
                self.canvas.coords(self.name2id[e['name']], e['M'])
                self.configure(self.name2id[e['name']], text=e['text'], font=e['font'])
            elif e['type'] == 'line':
                self.canvas.coords(self.name2id[e['name']], *e['bbox'])
            # Delete
            if e['type'] == 'delete':
                self.canvas.delete(self.name2id[e['name']])
        else:
            # Create
            if e['type'] == 'circle':
                self.name2id[e['name']] = self.canvas.create_oval(e['M'][0]-e['r'], e['M'][1]-e['r'], e['M'][0]+e['r'], e['M'][1]+e['r'])
            elif e['type'] == 'text':
                self.name2id[e['name']] = self.canvas.create_text(e['M'][0], e['M'][1], text=e['text'], font=e['font'])
            elif e['type'] == 'line':
                self.name2id[e['name']] = self.canvas.create_line(*e['bbox'])
 
               
if __name__ == '__main__':
    s=Screen(500, 500)
    s.process_element({'name':'horst', 'type':'circle', 'M':(100,400), 'r':40})
    s.process_element({'name':'hallo', 'type':'text', 'M':(200,200), 'text':'hallo', 'font':('Courier New', 40)})
    s.process_element({'name':'lydia', 'type':'line', 'bbox':(100,100,500,300)})
    s.update()
    sleep(1)
    s.input.put({'name':'horst', 'type':'circle', 'M':(400,400), 'r':40})
    s.input.put({'name':'hallo', 'type':'delete'})
    print(s.name2id)
    s.mainloop()
Oder wir lassen es, wie es war und untersuchen das Problem!

Also, die Frage, ob die Queue volläuft, oder das System nicht oder schlecht reagiert.
BlackJack

@Alfons Mittelmeyer: Es gibt keine Gewähr dafür das jeder Thread auf einem anderen Kern läuft, daraus folgt doch aber nicht Deine Beschreibung das grundsätzlich alle Threads den gleichen Kern verwenden. Es gibt auch keine Gewähr dafür das ein Thread seine ganze Lebensspanne auf dem gleichen Kern läuft. Und das hat auch alles nichts damit zu tun ob bei Dir Widgets ruckeln, das kann noch ganz andere Gründe haben. Und hat noch weniger mit dem Problem zu tun:

Wenn ein Producer-Thread schneller Daten produziert als ein Consumer-Thread sie verarbeiten kann, dann existiert das Problem. Dabei ist völlig egal wie Threads implementiert sind.
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

@Alfons: Deine Ausführungen zur Affinität von Kernen zu bestimmten Aufgaben ist Unsinn. So etwas gibt es unter Linux genauso wenig wie unter Windows. Und wenn man für userspace Programme daran rumfummelt, hat das oft konterintuitive Konsequenzen.

Und es ist mitnichten zu bezweifeln, dass Pythons Threads alle von mehreren Kernen übernommen werden. Auch wenn das ob des GIL ok ginge nur einen zu nutzen, nutzt Python die ganz normalen threading APIs, und die geben so etwas nicht her. Es ist also im Gegenteil davon auszugehen, das Threads auf wechselnden Kernen ausgeführt werden, ganz im Gegensatz zu deiner Behauptung.

Ob es hier sinnvoll ist auf threading zurück zu greifen ist in Python offensichtlich keine Frage der Parallelisierung bei CPU intensiver Arbeit. Es kann aber den Programmfluss einfacher machen, weil man nicht auf Ereignishäppchen umstellen muss. Das ist dann aber früher oder später eher ein Argument für greenlets und co. Wie es zB CCCP in Eve online & stackless Python machen.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Üpsilon hat geschrieben:(Vorsicht, viel Code :D )

Aber jetzt kommt der Hammer: Wenn ich das print in Zeile 57 auskommentiere, funktioniert gar nichts mehr. Man sieht dann nur ein leeres, hängendes Konsolenfenster und es öffnet sich KEIN Tkinter-Fenster. Habe keine Ahnung, wieso :( Wahrscheinlich hab ich irgendwas mit den Threads verkackt, aber ich sehe nicht, was genau. Wer kann helfen?

Danke im Voraus und freundliche Grüße!
Hallo Üpsilon man streitet hier im Forum mächtig herum und will alles besser wissen. Aber es ist Dein pollen und nicht weil die Queue volläuft. Die Lösung ist einfach:

Vergiss den Thread und die Hauptschleife sondern nimm einen Timer:

Code: Alles auswählen

from threading import Lock, Timer
Und hinten schreibst Du:

Code: Alles auswählen

    '''
    def hauptschleife(self):
        zeit = time()
        while self.spiel_laeuft:
            while time()-zeit < 1/FPS:
                pass
            zeit += 1/FPS
            self.schritt()
            self.ausgabe()
    '''

    def time_loop(self):
        self.schritt()
        self.ausgabe()
        if self.spiel_laeuft:
            Timer(1/FPS,self.time_loop).start()
 
rr = Rapid_Roll()

#Thread(target=rr.hauptschleife).start()
Timer(1/FPS,rr.time_loop).start()
rr.screen.mainloop()
Na, ist doch einfach, aber glauben will mir keiner. Dein seitwärts geht aber zumindest in der Eingangsversion nicht. Musst Du eben schauen.
BlackJack

@Alfons Mittelmeyer: Das macht aber nicht mehr das gleiche weil es 1/FPS Sekunden (+/-) wartet und da die Zeit für die beiden Methodenaufrufe *dazu* kommt. Alles zusammen soll aber maximal 1/FPS-Sekunden dauern. Zudem könnte man mit `sleep()` das gleiche wie `Timer` erreichen ohne für jeden Frame einen neuen Thread zu starten. Und noch besser wäre man würde gleich `after()` und gar keinen Thread verwenden.
Antworten