Heisenbug?

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

__deets__ hat geschrieben:All diese "Wissen" ist belegbar falsch. Deine drei Punkte oben treffen also zuerstmal auf dich selbst zu. :lol: :lol: :lol:
Du gehörst ganz sicher zu den Unbelehrbaren, da auch dann noch glauben, dass sie recht haben, wenn einwandfrei bewiesen ist, dass sie unrecht haben.

Und ein einwandfreier Beleg ist dieses Programm. Ich habe einen Raspberry Pi3 mit Linux und habe das print('x') rausgemacht.

Und was glaubst Du was kommt, wenn ich statt dem sleep das pass nehme?

Code: Alles auswählen

    def hauptschleife(self):
        zeit = time()
        while self.spiel_laeuft:
            while time()-zeit < 1/FPS:
                sleep(0.001) # statt pass
                #pass
            zeit += 1/FPS
            self.schritt()
            self.ausgabe()
Überhaupt keine GUI. Es gibt Betriebssysteme, welche gar keine Zeitscheibentechnik benützen, sondern erst einen Threadwechsel durchführen, wenn man ihn gestattet, etwa durch sleep. Bei einigen Windowsversionen weiß ich, dass das so war. Daß es allerdings bei meinem Raspberry auch so ist, hat mich doch überrascht. Hätte dieses Linux für besser gehalten. Oder liegt es an Python? Dafür bist ja Du anscheinenend nicht der Spezialist!

Ich würde mal denken, dass es nicht am Betriebssystem liegen sollte, ein Thread ist eben mal kein anderer Prozess. Und Python benutzt anscheinend keine Zeitscheibentechnik.
Zuletzt geändert von Alfons Mittelmeyer am Sonntag 30. Juli 2017, 22:17, insgesamt 1-mal geändert.
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

@Alfons Mittelmeyer: ich habe mal ein halbes Jahr lang intensiv Vögel beobachtet. Fliegen kann ich aber immer noch nicht.

Wenn es das System unterstützt, mißt Python bei sleep Mikrosekunden. Also auch wieder falsch geraten. Korrekterweise sieht die Schleife damit so aus:

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()
UPS: der Raspberry Pi3 hat einen Quad-Core Prozessor. Da scheint aber einer ganz schön viel Ahnung von Multiprocessing zu haben.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Sirius3 hat geschrieben: Wenn es das System unterstützt, mißt Python bei sleep Mikrosekunden. Also auch wieder falsch geraten. Korrekterweise sieht die Schleife damit so aus.
Also, das eine war Meßgenauigkeit. Python mag zwar eine Zeitmessung auf 1/10 Mikrosekunden angeben, aber die Zeitmessung alleine dauert bei meinem Raspberry Pi3 bereits etwa 5 Mikrosekunden.

So ein Ausdruck: a = zeit + 1/FPS
braucht bereits 10 Mikrosekunden.

Also Mikrosekundengenaue Zeitmessungen mit noch ein paar Statements hinzu kann man vergessen. (Und so etwas auch, dass nämlich ein Thread nur maximal 2 Mikrosekunden dauern darf - ein Beispiel aus Mobilfunk Software Anforderungen)

Aber darum geht es ja nicht. Was man bei sleep übergibt ist an sich egal. Hauptsache man ermöglicht einen Threadwechsel. Und der geht auch mit sleep(0)

Auch das kann man tun:

Code: Alles auswählen

    def hauptschleife(self):
        zeit = time()
        while self.spiel_laeuft:
            while time()-zeit < 1/FPS:
                sleep(0)
                #pass
            zeit += 1/FPS
            self.schritt()
            self.ausgabe()
Und wieviel Prozessorkerne der Pi3 hat, davon war nirgendwo die Rede. Die Rede war von Python Threads. Und vier Prozessorkerne ändern auch nichts daran, wie Python seine Threads abarbeitet.

Und zur Genauigkeit der Zeitmessung hier: wenn ein anderer Thread 1/10 Sekunde braucht, hat man mit seiner 1/50 Sekunde eben Pech gehabt.
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Alfons Mittelmeyer hat geschrieben: Überhaupt keine GUI. Es gibt Betriebssysteme, welche gar keine Zeitscheibentechnik benützen, sondern erst einen Threadwechsel durchführen, wenn man ihn gestattet, etwa durch sleep. Bei einigen Windowsversionen weiß ich, dass das so war. Daß es allerdings bei meinem Raspberry auch so ist, hat mich doch überrascht. Hätte dieses Linux für besser gehalten. Oder liegt es an Python? Dafür bist ja Du anscheinenend nicht der Spezialist!

Ich würde mal denken, dass es nicht am Betriebssystem liegen sollte, ein Thread ist eben mal kein anderer Prozess. Und Python benutzt anscheinend keine Zeitscheibentechnik.
Wie immer, viel Geschwaetz, keine Substanz.

Man muss ueber solche Sachen nicht spekulieren, das kann man wissen. Preemptives Scheduling ist seit NT in Windows kein Thema mehr, in Linux war es das noch nie. Und was Python benutzt muss man auch nicht raten, das kann man wissen, wenn man einfach mal in die Sourcen schaut: https://github.com/python/cpython/blob/ ... ead.h#L165

Sind halt POSIX-threads, wie von mir auch schon erwaehnt. "Oder liegt es an Python" ist vom selbsternannten Threading-Experten also ein etwas sehr erbaermliches Zeugnis.

Und dein Code-Beispiel enthaelt noch nicht mal GUI-Code, womit die Frage was du da verbockt hast oder nicht leider unbeantwortbar bleibt.

Folgender Code zeigt wider deiner Behauptung eine voellig responsive GUI auf einem Mehrkern-System, bei 100% CPU-Last auf einem Kern:

Code: Alles auswählen

import tkinter as tk
import threading


def spinner():
    while True:
        pass


def main():
    root = tk.Tk()
    frame = tk.Frame(root)
    frame.pack()
    button = tk.Button(
        frame,
        text="print", fg="red",
        command=lambda: print("foobar")
    )
    button.pack(side=tk.LEFT)
    t = threading.Thread(target=spinner)
    t.start()
    root.mainloop()

if __name__ == '__main__':
    main()
Bleibt also dabei: du redest viel, aber hauptsaechlich Unsinn....
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

__deets__ hat geschrieben:[Man muss ueber solche Sachen nicht spekulieren, das kann man wissen. Preemptives Scheduling ist seit NT in Windows kein Thema mehr, in Linux war es das noch nie. Und was Python benutzt muss man auch nicht raten, das kann man wissen, wenn man einfach mal in die Sourcen schaut: https://github.com/python/cpython/blob/ ... ead.h#L165

Sind halt POSIX-threads, wie von mir auch schon erwaehnt. "Oder liegt es an Python" ist vom selbsternannten Threading-Experten also ein etwas sehr erbaermliches Zeugnis.

Und dein Code-Beispiel enthaelt noch nicht mal GUI-Code, womit die Frage was du da verbockt hast oder nicht leider unbeantwortbar bleibt.

Folgender Code zeigt wider deiner Behauptung eine voellig responsive GUI auf einem Mehrkern-System, bei 100% CPU-Last auf einem Kern:

Bleibt also dabei: du redest viel, aber hauptsaechlich Unsinn....
Anscheinend hast Du null Ahnung von Unterschieden zwischen Multiprozessing, Multitasking und Multithreading und im Sourcecode stand nichts von Zeitscheibentechnik oder gar auf Prozessorkerne aufteilen. Die Threads laufen im selben Prozess wie Python auch und da ist nichts mit Multiprozessing auf Betriebssystemebene in Verbindung mit Threads.

Wenn Du Dir Wissen erwerben möchtest, wie Threads in Python arbeiten, dann findest Du hier eine kurze Einführung:
Siehe: https://www.slideshare.net/dabeaz/an-in ... oncurrency

Und Deine Witz Gui mit einem Button kannst Du als Widerlegung vergessen. Ja man kann den Button sogar noch mit after blinken lassen. Aber für den Canvas mit den movenden Balken und dem Ball reicht es nicht. Alles kommt an, kommt über die Queue die tkinter Commandos werden aufgerufen, aber tkinter hat keine Zeit für den Bildaufbau und damit geht dann nicht einmal mehr der Mausbutton Klick auf den Canvas.

Und dass ich keinen Code gezeigt hätte, stimmt nicht. Es ist genau der Code aus dem Eröffnungspost. Mit einem zusätzlichen sleep(0) geht das, weil damit der GIL (Globaler Interpreter Lock) freigegeben wird und damit kann wieder tkinter drankommen. Aber besser finde ich einen Timer statt Thread und Schleife mit sleep.
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Alfons Mittelmeyer hat geschrieben: Anscheinend hast Du null Ahnung von Unterschieden zwischen Multiprozessing, Multitasking und Multithreading und im Sourcecode stand nichts von Zeitscheibentechnik oder gar auf Prozessorkerne aufteilen. Die Threads laufen im selben Prozess wie Python auch und da ist nichts mit Multiprozessing auf Betriebssystemebene in Verbindung mit Threads.
Ich habe dir die Codezeile aus dem Python Interpreter gezeigt, der die Thread-Implementierung darstellt. Der geneigte Leser entnimmt dem, dass es sich um einen POSIX thread handelt, welcher sehrwohl "multiprozessing" wie du es so schoen falsch ausdrueckst betreibt. Du bleibst ahnungslos...
Alfons Mittelmeyer hat geschrieben: Und dass ich keinen Code gezeigt hätte, stimmt nicht. Es ist genau der Code aus dem Eröffnungspost. Mit einem zusätzlichen sleep(0) geht das, weil damit der GIL (Globaler Interpreter Lock) freigegeben wird und damit kann wieder tkinter drankommen. Aber besser finde ich einen Timer statt Thread und Schleife mit sleep.
Von dir selbst zusammengeklumpter Code den du irgendwo auf deiner Platte hast - der hat halt alle Moeglichen Fehler & taugt darum genau gar nix als "Beleg". Wo du deine kleine Endlosschleife eingebaut hast, kann hier keiner nachvollziehen....

Und "besser finde ich Timer", bei dem du jedes mal einen neuen Thread erzeugst (mit X system-calls), nur um am Ende nichts anderes als zu schlafen ... weiterer Beleg fuer Expertentum vom feinsten :mrgreen: :mrgreen: :mrgreen:
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

__deets__ hat geschrieben: Von dir selbst zusammengeklumpter Code den du irgendwo auf deiner Platte hast - der hat halt alle Moeglichen Fehler & taugt darum genau gar nix als "Beleg". Wo du deine kleine Endlosschleife eingebaut hast, kann hier keiner nachvollziehen....
Wir reden hier übder den Code vom User Üpsilon, falls Du das nocht nicht gemerkt haben solltest, und der steht hier:
viewtopic.php?f=18&t=40957

Oder hast Du das auch noch nicht geschnallt?
__deets__ hat geschrieben: Und "besser finde ich Timer", bei dem du jedes mal einen neuen Thread erzeugst (mit X system-calls), nur um am Ende nichts anderes als zu schlafen ... weiterer Beleg fuer Expertentum vom feinsten :mrgreen: :mrgreen: :mrgreen:
Ach so, jedes Mal ein neuer Thread. Ist da viel Unterschied, ob man den alten Thread in der Thread Queue drin läßt, oder ihn rauswirft (beendet) und wieder über einen Timer reinhängt?
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Alfons Mittelmeyer hat geschrieben: Wir reden hier übder den Code vom User Üpsilon, falls Du das nocht nicht gemerkt haben solltest, und der steht hier:
viewtopic.php?f=18&t=40957
Oder hast Du das auch noch nicht geschnallt?
Noe. Darueber reden wir nicht. Wir reden ueber irgendwelchen Klumpatsch, der auf deinem Rechner laeuft. Welche beliebigen Fehler du beim rauskopieren und rumfuhrwerken (GUI-Designer angeworfen?) gemacht hast, mag ich mir noch nicht mal in meinen kuehnsten Traeumen ausmahlen.

Abgesehen davon ist das fuer die Widerlegung deiner Behauptung nicht notwendig: mein Code reicht um zu zeigen, dass deine Theorie vom nur durch sleep(..) freizugebenden GIL Unfug ist. Das wuerde dann schon bei meinem Minimalbeispiel nicht klappen. Tut's aber, der Fehler liegt also bei dir - wo auch immer...
Alfons Mittelmeyer hat geschrieben: Ach so, jedes Mal ein neuer Thread. Ist da viel Unterschied, ob man den alten Thread in der Thread Queue drin läßt, oder ihn rauswirft (beendet) und wieder über einen Timer reinhängt?
Lass mich mal kurz ueberlegen... ein Systemaufruf (select mit timeout) https://github.com/python/cpython/blob/ ... le.c#L1444

vs einen neuen Thread anlegen (pthread_create), ein Condition-Objekt erzeugen, das hat ein Lock, etc etc etc... da kommen vielleicht ein dutzend teilweise recht komplexen Systemcalls zusammen, mit Resourcen-Anforderderungen diverser Natur.

Ja. Ist viel mehr Code. Ist deutlich mehr Aufwand. Bringt genau gar nix (fuer diesen Use-case). Aber du darfst das gefuehlt natuerlich besser finden, ist ja wichtig, das man sich im Recht *fuehlt*, nicht, das man es ist... QED dieser Thread (no pun intended).
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

__deets__ hat geschrieben: Abgesehen davon ist das fuer die Widerlegung deiner Behauptung nicht notwendig: mein Code reicht um zu zeigen, dass deine Theorie vom nur durch sleep(..) freizugebenden GIL Unfug ist. Das wuerde dann schon bei meinem Minimalbeispiel nicht klappen. Tut's aber, der Fehler liegt also bei dir - wo auch immer...
Sorry, dass ich das nicht bis in das Detail behandelt hatte, 'nur' durch sleep stimmt natürlich nicht. Es gibt auch Events und eine Event Queue und Events, wie binding an einen command und timer wie after werden natürlich behandelt. Hatte ja geschrieben, dass das mit 'after' und Abholen von der Queue klappt. Nur die Nachbearbeitung des Bildaufbaus durch tkinter also nach der Ausführung des Python codes, die klappt dann nicht mehr. Also nochmals- die Events werden abgearbeitet, nur nachdem der Python Code des Events abgearbeitet ist, ist Sense und tkinter kann den Bildaufbau nicht mehr machen. Endlich kapiert??? Also: Python meldet, der event ist fertig - doch tkinter ist noch nicht fertig, das ist das Problem hier.
Alfons Mittelmeyer hat geschrieben: Ach so, jedes Mal ein neuer Thread. Ist da viel Unterschied, ob man den alten Thread in der Thread Queue drin läßt, oder ihn rauswirft (beendet) und wieder über einen Timer reinhängt?
__deets__ hat geschrieben: Lass mich mal kurz ueberlegen... ein Systemaufruf (select mit timeout) https://github.com/python/cpython/blob/ ... le.c#L1444

vs einen neuen Thread anlegen (pthread_create), ein Condition-Objekt erzeugen, das hat ein Lock, etc etc etc... da kommen vielleicht ein dutzend teilweise recht komplexen Systemcalls zusammen, mit Resourcen-Anforderderungen diverser Natur.

Ja. Ist viel mehr Code. Ist deutlich mehr Aufwand. Bringt genau gar nix (fuer diesen Use-case). Aber du darfst das gefuehlt natuerlich besser finden, ist ja wichtig, das man sich im Recht *fuehlt*, nicht, das man es ist... QED dieser Thread (no pun intended).
Ja dieser Timer vom Threading gefällt mir auch nicht. Man sollte da lieber den after Timer von tkinter nehmen. Den kann man über event.set() auch mit anderen Threads verbinden.

Allerdings stellt sich jetzt dann die Frage, wozu ein anderer Thread.

Es gibt zwei grundsätzliche Konzepte:

- Es ist eine tkinter Anwendung: dann hat so ein anderer Thread nichts darin zu suchen und man nimmt einfach after ohne neuen Thread
- Die GUI könnte eine beliebige GUI sein, z.B. tkinter oder auch Qt oder eine andere.

Dann haben aber solche Bindings überhaupt nichts darin zu suchen:

self.screen.window.bind('<1>', self.on_L)

Außerdem diese locks sind völlig unsinnig:

Code: Alles auswählen

    def on_L(self, e):
        self.gedrueckt_lock.acquire()
        self.gedrueckt = -1
        self.gedrueckt_lock.release()
Außer dem Mousebutton 1 ruft das niemand auf.

Genauso ist dieser Lock völlig unsinnig:

Code: Alles auswählen

    def schritt(self):
        # Ball seitwärts
        self.gedrueckt_lock.acquire()
Außer in hauptschleife wird das nirgendwo anders aufgerufen.

Da das so ein Mischmasch ohne klares Konzept ist - und zudem mit nicht funktionierenden locks, ist zu empfehlen, den Thread ganz rauszuwerfen:

Code: Alles auswählen

    '''
    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):
        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()
Das hätte dann auch die beste Performance.
Auch after(1,...) in canvas_screen.py macht keinen Sinn. Für Werte unter 10 gibt es nicht den geringsten Grund.
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

@Alfons Mittelmeyer: jetzt hast Du den endgültigen Beweis geliefert, dass Du von Parallel-Programmierung keine Ahnung hast. Locks sind immer dann nötig, wenn mehrere Prozesse/Threads (oder wie Du es auch immer nennen magst; ist im wesentlichen das selbe), auf das selbe Objekt zugreifen, hier `self.gedrueckt`. Wir haben zum einen die Methoden on_xxx, die vom GUI-Thread aus aufgerufen werden, zum anderen `schritt`, das vom Spielupdate-Thread aus aufgerufen wird. Ohne Lock wäre es möglich, nicht-zulässige Zustände zu erzeugen (der Ball befindet sich außerhalb des Spielfeldes). Wir haben es also hier mit einem typischen Anwendungsfall von Locks zu tun, nach Schulbuch implementiert und völlig korrekt und notwendig.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Sirius3 hat geschrieben:@Alfons Mittelmeyer: jetzt hast Du den endgültigen Beweis geliefert, dass Du von Parallel-Programmierung keine Ahnung hast. Locks sind immer dann nötig, wenn mehrere Prozesse/Threads (oder wie Du es auch immer nennen magst; ist im wesentlichen das selbe), auf das selbe Objekt zugreifen, hier `self.gedrueckt`.
Natürlich sind die Locks nötig. Aber sie sind falsch implementiert. Diese Implementierung ist sinnlos.

Wenn Du etwas von Locks verstehen würdest, wüßtest Du dass man das so implementieren muss:

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
        self.ball[0] += self.on_gedrueckt()*SEITWAERTS_GESCHWINDIGKEIT
        if self.ball[0] < 0  or self.ball[0] > SPIELFELD_GROESSE:
            self.ball[0] -= self.on_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()
Außerdem habe ich die falsche Logik für seitwärts beseitigt. So läuft es
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Nachtrag:

Das stimmt auch noch nicht:

Code: Alles auswählen

    def schritt(self):
        # Ball seitwärts
        self.ball[0] += self.on_gedrueckt()*SEITWAERTS_GESCHWINDIGKEIT
        if self.ball[0] < 0  or self.ball[0] > SPIELFELD_GROESSE:
            self.ball[0] -= self.on_gedrueckt()*SEITWAERTS_GESCHWINDIGKEIT
self.on_gedrueckt könnte ja bereits gewechselt haben, daher:

Code: Alles auswählen

    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
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Wie ist das mit Python Threads tatsächlich?

__deets__ hat recht damit, dass Python Threads, was die Abarbeitung von Python Code anbelangt, sich nicht gegenseitig blockieren. Wenn es auf Pythonebene etwas zu tun gibt, dann wechselt der GIL zwischen den Threads. das sieht man hier:

Code: Alles auswählen

import tkinter as tk

import threading
from queue import Queue, Empty
from time import time, sleep

transfer = Queue()


class Application(tk.Tk):

    def __init__(self,**kwargs):
        tk.Tk.__init__(self,**kwargs)
        # widget definitions ===================================
        self.text = tk.Text(self)
        self.text.pack()
        self.poll_queue()

    def poll_queue(self):
        while True:
            try:
                output = transfer.get_nowait() + '\n'
                self.text.insert(tk.END,output)
            except Empty:
                break
        self.after(100, self.poll_queue)
        

def thread1():
    while True:
        zeit = time()
        while time()-zeit < 0.5:
            #sleep(0)
            pass
        transfer.put('1')


def thread2():
    while True:
        zeit = time()
        while time()-zeit < 0.5:
            #sleep(0)
            pass
        transfer.put('2')

t1 = threading.Thread(target=thread1)
t1.start()

t2 = threading.Thread(target=thread2)
t2.start()

Application().mainloop()
Das Problem ist aber, dass in Zeile 23 die Aktion nicht vollständig ausgeführt wird. Durch text.insert wird der insert in eine tkinter Datenstruktur eingetragen. Die grafische Darstellung aber erfolgt erst zeitlich versetzt auf Nichtpython Ebene. Nichtpython Code interessiert aber Python nicht und der GUI Thread bekommt daher den GIL nicht für die Nachbearbeitung der Grafikdarstellung.

Man sieht, dass thread1 und thread2 beide ausgeben und sich daher gegenseitig nicht blockieren. Die GUI allerdings funktioniert erst, wenn man bei beiden Threads das sleep reinnimmt.
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

@Alfons Mittelmeyer: das ursprüngliche Programm hat völlig korrekt mit Locks gearbeitet, und Du hast es geschafft, mit zwei Fehlversuchen im dritten Anlauf das Lock überflüssig zu machen, ohne es zu merken, und dabei aus einem klaren und nachvollziehbaren Interface ein Chaos zu veranstalten

on_gedrueckt hat nun zwei völlig unabhängige Funktionen: das setzen eines Attributs und das Abfragen eines Attributs; beide Operationen kann man (vorausgesetzt man verwendet einen Python-Bytecode-Interpreter mit GIL und interner Implementation der Wörterbücher) als atomar ansehen.
BlackJack

@Alfons Mittelmeyer: Es ist wieder diese komische und falsche Erklärung mit der unnötigen Unterscheidung zwischen Python- und Tk-Code. Das Problem hat damit nichts zu tun das der Python-Code ausgeführt wird aber Tk-Code nicht, sondern es werden einfach zu viele Elemente von den Producer-Threads in die Queue gesteckt, die vom Consumer-Thread nicht schnell genug abgearbeitet werden. Durch das `sleep()`, auch wenn es nur 0 ist, bekommt der Consumer-Thread genug Zeit ab um die Elemente in der Queue abzuarbeiten und damit die ``while``-Schleife auch tatsächlich mal zu verlassen, wordurch dann auch die GUI-Hauptschleife die GUI aktualisieren kann. Und das hat absolut nichts damit zu tun das die GUI-Hauptschleife und der Tk-Code in C und die Verarbeitung der Elemente in der Queue in Python implementiert sind. Wenn die GUI auch komplett in Python implementiert wäre, hätte man genau das gleiche Problem. Diese Unterscheidung in einer Erklärung macht keinen Sinn.

Edit: So ein `sleep()` ist aber keine Lösung, denn es kann auch sein das 0 nicht ausreicht und auch höhere, kleine Wartezeiten müssen nicht ausreichen. Es ist aber unmöglich vorherzusagen wie gross die Wartezeit sein muss, damit sie nicht zu klein ist, also muss man das Problem grundsätzlich angehen, und die Schleifendurchläufe begrenzen und in extremen Fällen vielleicht auch die Queue selbst in der Grösse begrenzen und damit die Producer-Threads dann blockieren lassen wenn sie voll ist.

@all: Bezüglich korrekter Verwendung von Locks würde ich noch ``with`` empfehlen, damit eine Ausnahme zwischen dem anfordern und freigeben einer Sperre selbige nicht im gesperrten Zustand hinterlassen kann.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Sirius3 hat geschrieben:@Alfons Mittelmeyer: das ursprüngliche Programm hat völlig korrekt mit Locks gearbeitet, und Du hast es geschafft, mit zwei Fehlversuchen im dritten Anlauf das Lock überflüssig zu machen, ohne es zu merken, und dabei aus einem klaren und nachvollziehbaren Interface ein Chaos zu veranstalten

on_gedrueckt hat nun zwei völlig unabhängige Funktionen: das setzen eines Attributs und das Abfragen eines Attributs; beide Operationen kann man (vorausgesetzt man verwendet einen Python-Bytecode-Interpreter mit GIL und interner Implementation der Wörterbücher) als atomar ansehen.
Stimmt, der Lock war richtig implementiert, denn es war ja der gleiche Lock self.gedrueckt_lock = Lock().

Aber wo siehst Du zwei Fehlversuche? Für das Funktionieren der GUI habe ich mehrere Lösungen gebracht, die alle funktioniert hatten, nämlich Timer, sleep und after. Am Besten man macht es mit after, wobei dann die Locks überflüssig sind. Und was hast Du gegen so ein klares Lock Interface mit Setzen und Abfragen durch dieselbe Funktion, das man nur einmal braucht? Bei einem Dictionary kann man mit my_dictionary[key] auch sowohl setzen wie abfragen oder mit config bei Tkinter auch. Ist also durchaus üblich für alle keys nur eine Funktion zu haben und dass man damit auch Setzen sowie Abfragen kann.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

BlackJack hat geschrieben:@Alfons Mittelmeyer: Es ist wieder diese komische und falsche Erklärung mit der unnötigen Unterscheidung zwischen Python- und Tk-Code. Das Problem hat damit nichts zu tun das der Python-Code ausgeführt wird aber Tk-Code nicht, sondern es werden einfach zu viele Elemente von den Producer-Threads in die Queue gesteckt, die vom Consumer-Thread nicht schnell genug abgearbeitet werden.
Also, die 0.5 Sekunden waren wohl zu hoch für Dich. Da nehmen wir lieber ganze Zahlen und mal sehen, ob Du den Code kapierst:

Code: Alles auswählen

import tkinter as tk

import threading
from queue import Queue, Empty
from time import time, sleep

transfer = Queue()


class Application(tk.Tk):

    def __init__(self,**kwargs):
        tk.Tk.__init__(self,**kwargs)
        # widget definitions ===================================
        self.text = tk.Text(self)
        self.text.pack()
        self.poll_queue()

    def poll_queue(self):
        while True:
            try:
                output = transfer.get_nowait() + '\n'
                self.text.insert(tk.END,output)
            except Empty:
                break
        self.after(1, self.poll_queue)
        

def thread1():
    while True:
        zeit = time()
        while time()-zeit < 1:
            #sleep(0)
            pass
        transfer.put('1')


def thread2():
    while True:
        zeit = time()
        while time()-zeit < 1:
            #sleep(0)
            pass
        transfer.put('2')

t1 = threading.Thread(target=thread1)
t1.start()

t2 = threading.Thread(target=thread2)
t2.start()

Application().mainloop()
In den Zeilen und 32 und 41 steht jeweils:

while time()-zeit < 1

Was beutet die "1"?

- 1 Mikrosekunde?
- 1 Millisekunde?
- 1 Sekunde?

Wieviele Daten pro Sekunde werden also in die Queue gelegt, wenn wir bedenken, dass wir zwei Threads haben?

In den Zeilen 19 bis 25 wird die Queue gepollt. Was bedeutet die 1 bei after in Zeile 26?

- 1 Stunde?
- 1 Minute?
- 1 Sekunde?
- 1 Millisekunde?

Kann es also passieren, dass das Abholen der Daten von der Queue nicht mit dem Draufladen mitkommt?
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Funktioniert ganz wunderbar wenn man ein update_idletasks einschiebt.

Code: Alles auswählen

import tkinter as tk

import threading
from queue import Queue, Empty
from time import time, sleep

transfer = Queue()


class Application(tk.Tk):

    def __init__(self,**kwargs):
        tk.Tk.__init__(self,**kwargs)
        # widget definitions ===================================
        self.text = tk.Text(self)
        self.text.pack()
        self.poll_queue()

    def poll_queue(self):
        while True:
            try:
                output = transfer.get_nowait() + '\n'
                self.text.insert(tk.END,output)
                self.text.update_idletasks()
            except Empty:
                break
        self.after(1, self.poll_queue)


def thread1():
    while True:
        zeit = time()
        while time()-zeit < 1:
            #sleep(0)
            pass
        transfer.put('1')


def thread2():
    while True:
        zeit = time()
        while time()-zeit < 1:
            #sleep(0)
            pass
        transfer.put('2')

t1 = threading.Thread(target=thread1)
t1.start()

t2 = threading.Thread(target=thread2)
t2.start()

Application().mainloop()
Wobei wunderbar natuerlich relativ ist, weil die beiden Threads die GUI trotzdem ziemlich zaeh machen. Aber das ist eine andere Baustelle. Den idle-task-Mechanismus von tk kenne ich nicht, aber du ja offensichtlich genauso wenig. Doch wenn man's nicht braeuchte, waere das Ding wohl kaum da.

Aber immer schoen andere der Inkompetenz bezichtigen, waehrend man speichel-spruehend Rueckzieher machen muss...
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

__deets__ hat geschrieben: Aber immer schoen andere der Inkompetenz bezichtigen, waehrend man speichel-spruehend Rueckzieher machen muss...
Wen willst Du jetzt damit gemeint haben? Das ist auch eine richtige Lösung, kann aber die GUI zäh machen. Wenn man an größere Queue Loads denkt und dass auch mehrere Queue Daten anstehen könnten, sollte man es lieber so machen:

Code: Alles auswählen

    def poll_queue(self):
        found = False
        while True:
            try:
                output = transfer.get_nowait() + '\n'
                self.text.insert(tk.END,output)
                found = True
            except Empty:
                break
        if found:
            self.update_idletasks()
        self.after(1, self.poll_queue)
In diesem Falle wird mit dem Befehl self.update_idletasks() der GUI Aufbau vollständig ausgeführt, sodass es einen Threadwechsel wieder hierher zurück, um den GUI Aufbau nachzuholen, nicht braucht. Ob man aber den GUI Aufbau bei größeren Datenloads jedesmal in sehr kurzen Abständen forcieren sollte, oder es tkinter überlassen sollte, ist die Frage. Mit sleep in den anderen Threads oder durch die Verwendung eines Timers, kann sich auch tkinter selber um den (evtl. optimalen) Zeitpunkt des Gui Aufbaus kümmern.
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

@__deets__: update_idletasks ist nur dazu da, damit man gegen das GUI-Rahmenwerk programmieren kann, um sich selber ins Knie zu schießen.

@Alfons Mittelmeyer: Deine "Lösung" ist, wie so oft, unsinnig. Wenn mehr Daten in die Queue kommen, als abgearbeitet werden, dann friert die GUI ein, weil sie nie aus der while-Schleife kommt. Kommt sie doch raus, dann ist "update_idletasks" (sowie das found-Flag) unnötig, weil poll_queue dann sowieso zu Ende ist und die GUI seine Ereignisschleife abarbeitet.
Das ist also identisch zu

Code: Alles auswählen

    def poll_queue(self):
        while True:
            try:
                output = transfer.get_nowait() + '\n'
                self.text.insert(tk.END,output)
            except Empty:
                break
        self.after(1, self.poll_queue)
poll_queue arbeitet im GUI-Thread, wo Du da einen Thread-Wechsel siehst, ist mir unklar.
Antworten