Heisenbug?

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

Alfons Mittelmeyer hat geschrieben: Und was willst Du mit Deinem Code Ausschnitt sagen:
...
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.
Ok, ich schreib das jetzt mal ganz langsam, weil du das ja offensichtlich etwas schwierig verstehst und darum ganz langsam lesen musst:

Das, lieber Alfons, ist ein Python Modul mit dem Namen "posix". Wenn das Betriebssytem SCHED_FIFO kennt, dann hat auch dieses Modul eine Konstante mit gleichem Namen. Damit *kann* man dann einen Aufruf machen, der die Thread-Prioritaet auf SCHED_FIFO setzt.

Aber nur weil diese Konstante im Code definiert wird heisst das noch lange nicht, dass sie auch benutzt wird! Es wird ja auch zB die Funktion shutil.rmtree definiert, mit der man sich seine gaaaaanze Festplatte loeschen koennte. Beim Start von Python. Tut man aber nicht, weil es niemand aufruft.

Und das gleiche gilt fuer SCHED_FIFO (bzw sched_param, das es benutzt) - es wird nirgendwo aufgerufen! Ausser in einem Test.

Langer Rede, kurzer Sinn: Es. Wird. Nicht. Aufgerufen. Und. Man. Muesste. ROOT. Sein. Damit. Es. Klappt. Was. Man. Aber. Ueblicherweise. Nicht. Ist.

Klar geworden? "Kapiert", wie du so schoen immer fragst?

Alfons Mittelmeyer hat geschrieben: 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.
Nochmal sehr langsam: auch bei deinem urspruenglichen Programm mit busy-waits OHNE sleep oder einem anderen Call kam der GUI-Thread "dran". Kannst ja mal ein print in poll_queue stopfen. Wird schoen regelmaessig aufgerufen. Das duerfte es laut deiner abstrusen Idee nicht.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

__deets__ hat geschrieben:Lieber das Schwadron auf volle Kanne, und losspekuliert.
Tja, wer da wild drauf losspekuliert, dürfte klar sein. Man hat den Code da, man kann in den Code ein print setzen und sich etwas ausgeben lassen. Dann weiß man, dass da kein Overload der Queue da ist, man kann auch das Reinschieben in die cQueue mit 1/50 swekunden also after(20,..) vergleichen mit after(1,..) beim Rausholen und zudem noch das Rausholen durch Schleife bis die Queue leer ist. Dann weiß man genau, dass da nichts zuviel in die Queue gelegt wird.

Dann hat man völlig falsche Vorstellungen von Mehrkernprozessoren in Verbindung mit Threads. Auf Kernelebene ist eine Aufteilung auf mehrere Prozessoren von POSIX Threads durchaus möglich.

Kannst es ja mal mit der Kithara Realtime Suite probieren, mit der ich entwickelt hatte:

http://kithara.com/de/loesungen/echtzeit-timer#tasks

Auf User Ebene geht da aber gar nichts. Ja mit FIFO hast Du recht. Dann ist es eben SCHED_OTHER, also diese Threads laufen, bis sie blockieren oder die Kontrolle abgeben (yield durch sleep und dergleichen, event in einem anderen Thread setzen ... ) oder durch einen Thread höherer Priorität unterbrochen werden. Jedenfalls ist es nicht SCHED_RR, bei dem es die Zeitscheibe gibt. Wenn man nicht blockiert oder die Kontrolle abgibt oder Python umwechselt (könnte etwa nach 100 Ticks durch event setzen geschehen, aber hab mir das nicht angeschaut) dann bleibt man im selben Thread.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

__deets__ hat geschrieben:
Alfons Mittelmeyer hat geschrieben: 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.
Nochmal sehr langsam: auch bei deinem urspruenglichen Programm mit busy-waits OHNE sleep oder einem anderen Call kam der GUI-Thread "dran". Kannst ja mal ein print in poll_queue stopfen. Wird schoen regelmaessig aufgerufen. Das duerfte es laut deiner abstrusen Idee nicht.
Was ist busy-waits? Das war Poll auf Zeit Schleife ohne sleep. Und natürlich war da der GUI Thread dran. Da wurde ja auch mit after ein Event getriggert, wodurch der Thread von waiting auf running übergeht und dann auch gleich aufgerufen wird.

Das after und das Abholen von der Queue funktioniert einwandfrei. Wie lange schreibe ich das schon!!! Aber danach bekommt der GUI Thread die Kontrolle nicht mehr, weil der oder die andern Threads keinen Threadwechsel zulassen, außer den durch Python Zeitscheibe, die aber anscheinend nur laufenden Python Code berücksichtigt und keine C-Code Ebene für tkinter intern.
Benutzeravatar
kbr
User
Beiträge: 1487
Registriert: Mittwoch 15. Oktober 2008, 09:27

Um dem Geschwafel hier etwas konkretes gegenüberzustellen: Es ist tatsächlich so, dass der tkinter-Thread im Thread-Scheduler zu kurz kommt; meine Vermutung warum, hatte ich schon gepostet. Mag der Thread-Scheduler Default-Einstellungen haben, so lassen sich diese seit Python 3.2 mit der neuen Funktion sys.setswitchinterval() bezüglich der Laufzeit beeinflussen (das ältere sys.setcheckinterval wird nicht mehr unterstützt). Dann ist auch kein sleep() mehr erforderlich, um zu signalisieren, dass ein potentieller blocking-call erfolgt und der Thread-Scheduler vorzeitig aktiv werden kann.

Code: Alles auswählen

import tkinter as tk

import threading
import sys
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)
        self.text = tk.Text(self)
        self.text.pack()
        self.poll_queue()

    def poll_queue(self):
        while True:
            sys.setswitchinterval(0.2)  # give tkinter time
            try:
                output = transfer.get_nowait() + '\n'
            except Empty:
                break
            else:
                self.text.insert(tk.END,output)
        self.after(100, self.poll_queue)


def thread1():
    while True:
        zeit = time()
        while time()-zeit < 1:
            sys.setswitchinterval(0.0001)  # don't run mad without switching
            pass
        transfer.put('1')


def thread2():
    while True:
        zeit = time()
        while time()-zeit < 1:
            sys.setswitchinterval(0.0001)  # dito
            pass
        transfer.put('2')


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

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

Application().mainloop()
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Ob es dein Bug ist, darüber kann man streiten. Wer in C programmiert, sollte wissen, dass da ein wait erforderlich ist. In Python ist das ein sleep. Damit gibt es auch kein Problem. Dass Python auch nach Zeitscheibe wechselt ist eine zusätzlich Userunterstützung, wenn er sleep nicht macht. Diese Userunterstützung ist aber nur halbherzig implementiert, weil sie nur Threads mit laufendem Python Code berücksichtigt und andere Threads, die auf Nichtpython Ebene auf Ausführung warten, nicht.

Tja, Bug oder nicht Bug, das ist hier die Frage. Jedenfalls ist es falsch, sleep nicht zu benutzen, auch wenn es richtig implementiert wäre, weil das die Performance herunterdrückt.

Auch drückt ein anderer Thread die Performance herunter wenn man in diesem sleep verwendet. sleep(0) heißt nicht, dass man ganz schnell wieder dran ist, sondern heißt, dass man dann wieder dran ist, wenn der andere Thread, der auch etwas zu tun hat, fertig ist. Und das ist bei der GUI eventuell ein neuer Bildaufbau, bevor man mit sleep(0) wieder dran ist. Besser ist jedenfalls selber Thread und triggern mit after, da das ein Event ist und Priorität hat gegenüber einem neuen Bildaufbau. Der Bildaufbau kann das eventuell noch berücksichtigen und wenn nicht, dann eben das nächste Mal. Jedenfalls kann man schon einmal die neuen Werte für denselben oder den nächsten Bildaufbau ohne Stocken übermitteln.
Antworten