Heisenbug?

Fragen zu Tkinter.
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: 17754
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: 14545
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: 17754
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.
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Sirius3 hat geschrieben:ommt 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.
Nur behauptet, oder auch getestet? Ich habe das update_idletasks nicht umsonst eingefuegt, deine Version ist gleich zu der von Alfons, und die updatet die GUI tatsaechlich NICHT. Warum weiss ich nicht, wie ich ja schon schrieb.
Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

@__deets__: gegen Deine Version hatte ich ja gar keine Einwände. Es kann nur sein, dass das Einfügen vom Text noch viel langsamer wird, weil nach jeder Zeile einmal ein Update kommt (das ist vielleicht auch gewollt).
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ich habe gar keine updates bekommen. Ohne update_idletasks kommt bei voller Threadlast noch nicht mal das Fenster hoch. Warum das nun wiederum der Fall ist, und sich zb beim aendern vom busy-loop-wait auf sleep(1) dann doch geht (oder beim print des TE) - keine Ahnung. Das mag an der Art liegen, wie tkinter arbeitet. Mit den Erklaerungen aus Absurdistan hat das natuerlich nix zu tun, aber was da wie ineinander greift ist mir auch nicht klar.
Benutzeravatar
kbr
User
Beiträge: 1487
Registriert: Mittwoch 15. Oktober 2008, 09:27

@__deets__ : Ohne es genau zu wissen, habe ich eine andere Vermutung für das Verhalten. In einem Vortrag von David Beasley meine ich gehört zu haben, dass es ein Implementierungsdetail des Python-Interpreters sei, bei einem Threadwechsel Threads, welche eine hohe Prozessorlast verursachen, zu bevorzugen. Nun weiß ich nicht, wo dies im Python-Quellcode implementiert ist (und weiß auch nicht, ob ich dies mal eben durch Einsicht in den Quellcode verifizieren könnte), falls aber ein Aufruf von sleep() in einem Thread, unabhängig von dem sleep übergebenen Argument, Einfluss auf diese Gewichtung nimmt, so wäre das Verhalten für mich erklärbar. Die maximal ausführbare Anzahl an Anweisungen, die Python einem Thread zugesteht, ändert sich nicht, der Haupt-Thread, in welchem der tkinter-Code läuft, käme aber weniger oft zum Zuge. Dessen Code ist auch deutlich umfangreicher, so dass die tcl-calls zum updaten der GUI vermutlich nur selten erreicht werden. Dies wäre wohl ein typischer Fall für Multiprocessing. Der ganze IPC Overhead lohnt sich dann.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Sirius3 hat geschrieben: @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..
Wie soll mehr in die Queue reinkommen, als herausgelesen wird? Ja das wäre eine Endlosschleife, wenn jemand mehr Daten in die GUI hineinschieben kann, als man mit Python herauslesen kann. Aber ein C Programmm ist hier nirgends am Werk.

Aber solange hier aus der Queue herausgelesen wird, ist so schnell niemand anders dran. Und wenn sie dran sind, dann schieben sie, nicht wie in diesem langsamen Fall sondern wie in des Users Fall nur Daten im Abstand von 1/50 Sekunde rein also lange nicht so schnell, wie man sie in einer Endlosschleife (- aber nur bis empty) abholen kann.
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

kbr hat geschrieben:@__deets__ : Ohne es genau zu wissen, habe ich eine andere Vermutung für das Verhalten. In einem Vortrag von David Beasley meine ich gehört zu haben, dass es ein Implementierungsdetail des Python-Interpreters sei, bei einem Threadwechsel Threads, welche eine hohe Prozessorlast verursachen, zu bevorzugen.
Wenn das stimmt ist mir zum einen voellig unklar, wie Python es da moeglich macht, einen solchen Einfluss zu nehmen.

So oder so sollte es aber nicht dazu fuehren, dass ein Thread *gar* nicht mehr dran kommt, und das tut es ja auch nicht. Der Tk-Thread kommt sehr wohl dran. Die hohe Threadlast scheint eher irgendeinen Timeout in Tk zu ueberfordern. Die optimieren indem sie Kaskaden von updates erstmal abwarten, um dann "in aller Ruhe" mal die GUI neu zu malen. Und genau dieses Ding wird ausgehebelt. Es wird also "einfach" kein update idle event in die event-queue von Tk gepackt - etwas, das ich dann erzwinge.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

__deets__ hat geschrieben:
kbr hat geschrieben:@__deets__ : Ohne es genau zu wissen, habe ich eine andere Vermutung für das Verhalten. In einem Vortrag von David Beasley meine ich gehört zu haben, dass es ein Implementierungsdetail des Python-Interpreters sei, bei einem Threadwechsel Threads, welche eine hohe Prozessorlast verursachen, zu bevorzugen.
Wenn das stimmt ist mir zum einen voellig unklar, wie Python es da moeglich macht, einen solchen Einfluss zu nehmen.

So oder so sollte es aber nicht dazu fuehren, dass ein Thread *gar* nicht mehr dran kommt, und das tut es ja auch nicht. Der Tk-Thread kommt sehr wohl dran. Die hohe Threadlast scheint eher irgendeinen Timeout in Tk zu ueberfordern. Die optimieren indem sie Kaskaden von updates erstmal abwarten, um dann "in aller Ruhe" mal die GUI neu zu malen. Und genau dieses Ding wird ausgehebelt. Es wird also "einfach" kein update idle event in die event-queue von Tk gepackt - etwas, das ich dann erzwinge.
Es ist keine hohe Threadlast, wenn man im Einführungsbeispiel im zweiten Listing sleep(0) verwendet und im ersten after(100,...) dann ist das auch kein Problem mit der Queue, nur sieht die Zehntelsekunde nicht gut aus, weil es ruckelt. Schon gemerkt, dass da in einer Schleife alle Daten in der Queue abgeholt werden? Aber after(20,...) ist ausreichend schnell auf HD Monitoren. Die 40 wäre vielleicht noch für SD Auflösung geeignet, macht sich aber auf HD nicht mehr gut.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Die Erklärung ist ein Bug in Python.

Es handelt sich unter Linux um POSIX Threads. Der GIL ist unter Linux oft implementiert als Semaphore. POSIX Treads lassen sich konfigurieren. Die Konfiguration FIFO läuft solange, bis ein Thread die Kontrolle abgibt. Bei einer RR Konfiguration kommt hinzu, dass nach einem definierten Quantum (Zeit) der Thread gewechselt wird.

Offensichtlich handelt es sich um eine FIFO Konfiguration, denn ansonsten würde der GUI Thread dran kommen. Bei Threads können wir zwei Zustände unterscheiden:

- waiting
- running

Waiting bedeutet, dass der Thread etwa wartet, bis ein Lock frei wird, ein Event ankommt, eine Zeit abgelaufen ist oder eine IO Operation fertig ist. Danach kommt der Thread in eine Queue der running Threads.

Bei sleep(0) wird der Thread unterbrochen und wird gleich wieder hinten an die Queue der running Threads angefügt. Bei Werten ungleich 0 erst über einen Timer, nachdem diese Zeit abgelaufen ist.

Python selber überprüft, ob in running Threads etwas läuft. Das erfolgt im Abstand von 100 Ticks, was immer das auch sein mag. Anscheinend ist das aber falsch implementiert, denn Python interessiert sich nicht dafür, welche Threads warten, dass die drankommen, sondern offensichtlich nur für die, in welchen der Pythoninterpreter läuft.

Ein sleep(0) geht über das Betriebssystem und das läßt auch den GUI Thread dran - der ja auch mit sleep (in C wait) wartet. Das Umschalten nach 100 Ticks geht über Python und Python interessiert sich anscheinend nur für Threads in denen Python läuft.

Das ist ein eindeutiger Implementierungsfehler in Python.

Es gibt also von Python aus eine Zeitscheibe, aber nur für Python Code.
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Alfons Mittelmeyer hat geschrieben: Offensichtlich handelt es sich um eine FIFO Konfiguration, denn ansonsten würde der GUI Thread dran kommen. Bei Threads können wir zwei Zustände unterscheiden:
Unfug. SCHED_FIFO benoetigt root-Rechte. Und wird auch nicht automatisch gesetzt, das waere ja fatal. Damit schiesst man sich ja sein ganzes System zu klump. Und zeig mir mal die Stelle, wo das im Python code vorkommt. Hier, ich mach's einfach fuer dich:

https://github.com/python/cpython/searc ... FIFO&type=

Last but not least: DER THREAD KOMMT DRAN!!!!!!!!!! AUCH WENN MAN GAR KEIN SLEEP MACHT, SONDERN BUSY WAITS.

Tkinter entscheidet sich halt nicht dafuer, ein update durchzufuehren. Warum weiss hier keiner, da es aber klappt wenn man es dazu zwingt, ist deine schoene Theorie fuer die Ritze. Aber rezipieren von Fakten - schwieeeeeerig. Lieber das Schwadron auf volle Kanne, und losspekuliert.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

__deets__ hat geschrieben:
Alfons Mittelmeyer hat geschrieben: Offensichtlich handelt es sich um eine FIFO Konfiguration, denn ansonsten würde der GUI Thread dran kommen. Bei Threads können wir zwei Zustände unterscheiden:
Unfug. SCHED_FIFO benoetigt root-Rechte. Und wird auch nicht automatisch gesetzt, das waere ja fatal. Damit schiesst man sich ja sein ganzes System zu klump. Und zeig mir mal die Stelle, wo das im Python code vorkommt. Hier, ich mach's einfach fuer dich:

https://github.com/python/cpython/searc ... FIFO&type=
Und was willst Du mit Deinem Code Ausschnitt sagen:

Code: Alles auswählen

#ifdef HAVE_SCHED_H
#ifdef SCHED_OTHER
    if (PyModule_AddIntMacro(m, SCHED_OTHER)) return -1;
#endif
#ifdef SCHED_FIFO
    if (PyModule_AddIntMacro(m, SCHED_FIFO)) return -1;
#endif
#ifdef SCHED_RR
    if (PyModule_AddIntMacro(m, SCHED_RR)) return -1;
#endif
Dass es abhängig von Compilerflags ist, welches Scheduling verwendet wird? Ja, wie der Compiler aufgerufen wird, steht wohl nicht im C-Code. Da müßtest Du schon auf das Makefile verweisen.
__deets__ hat geschrieben: Last but not least: DER THREAD KOMMT DRAN!!!!!!!!!! AUCH WENN MAN GAR KEIN SLEEP MACHT, SONDERN BUSY WAITS.

Tkinter entscheidet sich halt nicht dafuer, ein update durchzufuehren. Warum weiss hier keiner, da es aber klappt wenn man es dazu zwingt, ist deine schoene Theorie fuer die Ritze. Aber rezipieren von Fakten - schwieeeeeerig. Lieber das Schwadron auf volle Kanne, und losspekuliert.
Es ist doch völlig egal mit was Du wartest, ob mit sleep oder etwas anderem. Hauptsache Du wartest mit etwas, anstatt mit Endlosschleife zu laufen, dann kommen nämlich auch die anderen Threads dran.
Antworten