Blockierung von Canvas-Methoden durch parallele Threads

Fragen zu Tkinter.
Antworten
spooky0815
User
Beiträge: 23
Registriert: Montag 27. Februar 2006, 19:24

Montag 1. Januar 2007, 20:44

Hallo,

Ich habe wiedermal ein kleines großes Problem:

ich habe eine GUI programmiert, in der ich während der Laufzeit neue Grafik-Objekte erstellen kann, welche dann jeweils in einem getrennten thread ihre Arbeit verrichten...Alles funktioniert relativ gut :)

Wenn nun allerdings die Anzahl der Objekte, also auch Threads, hinreichend steigt, treten unregelmäßig Fehlermeldungen von Methoden der Canvas-Klasse auf (coords(), find_withtag(), itemconfigure(), etc.). Höchstwahrscheinlich rufen dann mehrere Threads die selbe Methode im gemeinsam verwendeten Canvas-Objekt gleichzeitig auf und es kommt zu Zugriffsverletzungen.

Deshalb suche ich verzweifelt nach einer Möglichkeit, dass die Threads zwar weiterhin unabhängig von einander arbeiten können, aber keine Zugriffsverletzungen mehr auftreten.

Mir kommen 2 Ideen in den Kopf, für die ich aber leider noch keine Lösung en kenne:

1) Eine Sperre / Lock, durch die ein exklusiver Zugriff auf die Canvas-Methoden gewährleistet ist. Alle anderen zugreifenden Threads müssen warten / werden vom OS scheduled.

2) Eine Art Befehls-Queue, in die ich alle Methodenaufrufe schiebe, wodurch ebenfalls der exklusive Zugriff auf die Canvas-Methoden erreicht wird.


Bsp-fehlermeldungen:

File "C:\Programme\Python\lib\lib-tk\Tkinter.py", line 2090, in create_line
return self._create('line', args, kw)
File "C:\Programme\Python\lib\lib-tk\Tkinter.py", line 2076, in _create
return getint(self.tk.call(
ValueError: invalid literal for int(): 1383expected boolean value but got "??"

File "C:\Programme\Python\lib\lib-tk\Tkinter.py", line 2146, in find_withtag
return self.find('withtag', tagOrId)
File "C:\Programme\Python\lib\lib-tk\Tkinter.py", line 2119, in find
return self._getints(
File "C:\Programme\Python\lib\lib-tk\Tkinter.py", line 972, in _getints
return tuple(map(getint, self.tk.splitlist(string)))
ValueError: invalid literal for int(): expected

for i in self.c.find_withtag('red'):
File "C:\Programme\Python\lib\lib-tk\Tkinter.py", line 2146, in find_withtag
return self.find('withtag', tagOrId)
File "C:\Programme\Python\lib\lib-tk\Tkinter.py", line 2119, in find
return self._getints(
File "C:\Programme\Python\lib\lib-tk\Tkinter.py", line 972, in _getints
return tuple(map(getint, self.tk.splitlist(string)))
ValueError: invalid literal for int(): 7006expected


... zu beachten ist, dass die ValueErrors im Quellcoed definitiv nicht zutreffend sind...d.h. sie können eigentlich nur durch Zugriffsverletztungen, etc. entstehen (oder irre ich mich ?? :-)

Ich wäre also für Tipps / Lösungen sehr dankbar.

Viele Grüße,

spooky0815
BlackJack

Montag 1. Januar 2007, 21:03

In den meisten GUIs die ich kenne darf man nur von einem Thread aus auf die GUI zugreifen. Ich würde die Queue-Lösung implementieren.
spooky0815
User
Beiträge: 23
Registriert: Montag 27. Februar 2006, 19:24

Montag 1. Januar 2007, 21:26

> Ich würde die Queue-Lösung implementieren.

Ok danke...könntest Du mir da evtl. Tipps geben. Oder hat jemand einen entsprechenden Codesnippets?

VG

Spooky0815
spooky0815
User
Beiträge: 23
Registriert: Montag 27. Februar 2006, 19:24

Dienstag 2. Januar 2007, 16:11

Hallo nochmal,

Ich habe jetzt schon einige Sachen getestet bzw. recherchiert.

Mein 1. Versuch war der Einsatz einer normalen globalen Lock-Variable. Im nachhinein sehr dumm, da die Threads ja beliebig vom Betriebssystem bedient werden können. D.h. mit einer normalen Variable konnte es nie funktionieren.

Mein 2. Versuch mit einem Lock-Objekt:

l=thread.acquire_lock()
lock.acquire()

<tue etwas, dass nicht unterbrochen werden darf>

lock.release()

brachte keinen Erfolg. Ich habe daraufhin im Netz gelesen, das Python nicht vollkommen "Thread-Safe" ist wie bspw. Java. Ich weiß nun nicht, ob es daran liegt, dass es mit Locks nicht funktioniert :(

Mein 3. Versuch bezog sich auf die Aussage, das nur ein Thread auf die Grafik-Engine zugreifen kann...Ich habe mir daraufhin in meiner Grafikklasse Methoden erstellt, um Objekte zu zeichnen, zu ändern, etc. Alle Threads nutzen die gleiche "Umwelt" und greifen nun auf die erstellten Methoden zu, anstatt selbst irgendwelche Canvas-Methoden direkt zu rufen.
Ich ging davon aus, dass Sie dann korrekt nach dem Fifo-Prinzip abgearbeitet werden...leider hatte ich auch so keinen Erfolg :(

Ich bitte daher nochmal um evtl. Lösungsmöglichkeiten. Eine gemeinsame Queue für Kommandos klingt gut, allerdings weiß ich hier nicht, wie man eine Queue direkt Befehle ausführen lassen kann.

Ich weiß, dass bei der Queue das funktioniert:

q.put('A')
q.put('B')
q.put('C')

...

print q.get()
> A
print q.get()
> B

...


Aber ich möchte ja etwas in dieser Richtung - alle Threads schreiben ihre Anweisungen in eine gemeinsame Queue:

q.put(c.itemconfig(....))
q.put(c.delete(....))
q.put(c.find_withtag(....))

Diese Befehle sollen dann nacheinander abarbeitet werden, um keine Zugriffsverletzungen zu erhalten.

Ist so etwas überhaupt möglich? Wenn nicht welche Alternativen gibt es? Oder bin ich der erste, der solche Probleme in Multithreaded GUIs zu lösen hat :((

Ich wäre für jede Hilfe dankbar!

VG

spooky0815
BlackJack

Dienstag 2. Januar 2007, 17:37

spooky0815 hat geschrieben:Ich habe daraufhin im Netz gelesen, das Python nicht vollkommen "Thread-Safe" ist wie bspw. Java.
Das ist Blödsinn. Python ist sogar (leider) sicherer, da wegen dem `global interpreter lock` weniger Nebenläufigkeit besteht als eigentlich möglich wäre.

Und AWT und Swing sind auch nicht "thread safe", auch bei Java sollte man nur von einem Thread aus auf die GUI zugreifen.
Mein 3. Versuch bezog sich auf die Aussage, das nur ein Thread auf die Grafik-Engine zugreifen kann...Ich habe mir daraufhin in meiner Grafikklasse Methoden erstellt, um Objekte zu zeichnen, zu ändern, etc. Alle Threads nutzen die gleiche "Umwelt" und greifen nun auf die erstellten Methoden zu, anstatt selbst irgendwelche Canvas-Methoden direkt zu rufen.
Ich ging davon aus, dass Sie dann korrekt nach dem Fifo-Prinzip abgearbeitet werden...leider hatte ich auch so keinen Erfolg :(
Das bringt natürlich so nichts weil die ja trotzdem noch gleichzeitig von den verschiedenen Threads aufgerufen und abgearbeitet werden. Was Du machen kannst, ist diese Methoden mit einen `Lock` gegeneinander zu schützen. Also jede Methode muss am Anfang das Lock anfordern und am Ende wieder freigeben. Das ist letztendlich das was Java intern macht, wenn man eine Methode als ``synchronized`` deklariert.
Aber ich möchte ja etwas in dieser Richtung - alle Threads schreiben ihre Anweisungen in eine gemeinsame Queue:

q.put(c.itemconfig(....))
q.put(c.delete(....))
q.put(c.find_withtag(....))

Diese Befehle sollen dann nacheinander abarbeitet werden, um keine Zugriffsverletzungen zu erhalten.

Ist so etwas überhaupt möglich?
Ganz so natürlich nicht, Du packst hier die Ergebnisse der Aufrufe in die Queue. Was Du machen kannst, ist die Aufrufe durch ``lambda`` zu "schützen" oder ein Tupel mit Funktionsobjekt und Argumenten in die Queue zu packen und dann einen Thread erstellen, der diese Tupel aus der Queue holt und die Funktionsobjekte mit den Argumenten aufruft.
spooky0815
User
Beiträge: 23
Registriert: Montag 27. Februar 2006, 19:24

Dienstag 2. Januar 2007, 20:21

Das bringt natürlich so nichts weil die ja trotzdem noch gleichzeitig von den verschiedenen Threads aufgerufen und abgearbeitet werden. Was Du machen kannst, ist diese Methoden mit einen `Lock` gegeneinander zu schützen. Also jede Methode muss am Anfang das Lock anfordern und am Ende wieder freigeben. Das ist letztendlich das was Java intern macht, wenn man eine Methode als ``synchronized`` deklariert.
Ok...würde das gerne testen...Kannst du mir ein kleines Code-Schnipsel geben, wie sowas aussehen könnte, damit ich zumindest in Sachen Syntax auf der sicheren Seite stehe. :) Ich brauche sicherlich nur ein gemeinsames Lock-Objekt, oder?
Was Du machen kannst, ist die Aufrufe durch ``lambda`` zu "schützen" oder ein Tupel mit Funktionsobjekt und Argumenten in die Queue zu packen und dann einen Thread erstellen, der diese Tupel aus der Queue holt und die Funktionsobjekte mit den Argumenten aufruft.
... Hiilfe...das versteh ich nicht mehr :( Wie würde sowas aussehen? D.h. wie kann ich Funktionsobjekte mit Argumenten aus der Queue füttern?? Hab in die Richtung noch nichts gemacht. Hat das was mit self.func() zu tun??

Vielen Dank schonmal und viele Grüße,

spooky0815
BlackJack

Dienstag 2. Januar 2007, 21:57

spooky0815 hat geschrieben:
Also jede Methode muss am Anfang das Lock anfordern und am Ende wieder freigeben. Das ist letztendlich das was Java intern macht, wenn man eine Methode als ``synchronized`` deklariert.
Ok...würde das gerne testen...Kannst du mir ein kleines Code-Schnipsel geben, wie sowas aussehen könnte, damit ich zumindest in Sachen Syntax auf der sicheren Seite stehe. :) Ich brauche sicherlich nur ein gemeinsames Lock-Objekt, oder?
Ja genau. Wenn das alles Methoden einer Klasse sind, dann kannst Du zum Beispiel in der `__init__()` ein `Lock`-Objekt an `self.lock()` binden. Die Methoden die intern dann auf das `Canvas`-Objekt zugreifen müssen sich die Sperre vorher holen und danach wieder freigeben. Also nach folgendem Muster:

Code: Alles auswählen

    def do_something(self, spam):
        self.lock.aquire()
        # Do something with canvas.
        self.lock.release()
Vor und nach den "Lock"-Zeilen kann auch noch Code stehen, solange der nicht auf den `Canvas` zugreifen.
... Hiilfe...das versteh ich nicht mehr :( Wie würde sowas aussehen? D.h. wie kann ich Funktionsobjekte mit Argumenten aus der Queue füttern??
Das geht z.B. so:

Code: Alles auswählen

In [72]: int('dead', base=16)
Out[72]: 57005

In [73]: q = Queue.Queue()

In [74]: q.put((int, ['dead'], { 'base': 16 }))

In [75]: func, args, kwargs = q.get()

In [76]: func(*args, **kwargs)
Out[76]: 57005
spooky0815
User
Beiträge: 23
Registriert: Montag 27. Februar 2006, 19:24

Mittwoch 3. Januar 2007, 09:51

Hallo,

Erstmal danke für die Tipps. Gerade die 2. Möglichkeit ist ja wirklich interessant.

Aber noch eine kurze Frage zur 1. Methode mit Lock:

Ich habe das ganze umgesetzt und habe subjektiv auch den Eindruck, dass es deutlich weniger Fehler hervorbringt. Leider kommt aber manchmal immernoch folgender Fehler in meiner eigentlich "gelockten" drawLine-Methode:
File "C:\...", line 382, in drawLine
self.c.create_line(x1, y1, x2, y2, fill=fill, tag=tag, width=width)
File "C:\Programme\pyton21\lib\lib-tk\Tkinter.py", line 2090, in create_line
return self._create('line', args, kw)
File "C:\Programme\pyton21\lib\lib-tk\Tkinter.py", line 2076, in _create
return getint(self.tk.call(
ValueError: invalid literal for int(): 558expected boolean value but got "??"
sehr komisch :( Scheinbar immer noch ein Problem. Die Funktion sieht so aus (lck wurde in __init__ als self.lck=Lock() definiert):

Code: Alles auswählen

def drawLine(self, x1,y1,x2,y2,fill,tag,width):
       self.lck.acquire()
       self.c.create_line(x1, y1, x2, y2, fill=fill, tag=tag, width=width)
       self.lck.release()
### off-topic: ###

Weiterhin hab ich vorhin folgendes Mini-Script in der Console (Windows) getestet und war sehr erstaunt über das Ergebnis:

Code: Alles auswählen

def a(x):
    for i in range(0,10):
        print x
        time.sleep(.1)

a('XYZ')
> funktioniert

Code: Alles auswählen

thread.start_new_thread(a,('XYZ',))
> nach dem ersten Schleifendurchlauf bricht das Script ohne Fehlermeldung ab. Ohne das sleep() funktioniert alles prima.

Wie das? Zu bemerken ist, dass das ganze nur in der Std-Windows-Console auftritt und nich in der Python-Shell oder der grafischen Python-Konsole. Weiß jmd. warum das so ist?

Vielen Dank und viele Grüße,

spooky0815
BlackJack

Mittwoch 3. Januar 2007, 12:13

spooky0815 hat geschrieben:Aber noch eine kurze Frage zur 1. Methode mit Lock:

Ich habe das ganze umgesetzt und habe subjektiv auch den Eindruck, dass es deutlich weniger Fehler hervorbringt. Leider kommt aber manchmal immernoch folgender Fehler in meiner eigentlich "gelockten" drawLine-Methode:
File "C:\...", line 382, in drawLine
self.c.create_line(x1, y1, x2, y2, fill=fill, tag=tag, width=width)
File "C:\Programme\pyton21\lib\lib-tk\Tkinter.py", line 2090, in create_line
return self._create('line', args, kw)
File "C:\Programme\pyton21\lib\lib-tk\Tkinter.py", line 2076, in _create
return getint(self.tk.call(
ValueError: invalid literal for int(): 558expected boolean value but got "??"
Hm, wenn ich so recht überlege greifen da immer noch zwei Threads auf Tk zu, der in dem die `mainloop()` läuft ja auch. Grmpf.

Da sehe ich nur zwei Lösungen mit einer Queue, entweder die `after()`-Methode von Widgets benutzen um die Queue zu "pollen" oder eine eigene `mainloop()` schreiben, die neben `Tkinter.Tk.update()` auch die Queue abfragt.

Code: Alles auswählen

def a(x):
    for i in range(0,10):
        print x
        time.sleep(.1)

a('XYZ')
> funktioniert

Code: Alles auswählen

thread.start_new_thread(a,('XYZ',))
> nach dem ersten Schleifendurchlauf bricht das Script ohne Fehlermeldung ab. Ohne das sleep() funktioniert alles prima.

Wie das? Zu bemerken ist, dass das ganze nur in der Std-Windows-Console auftritt und nich in der Python-Shell oder der grafischen Python-Konsole. Weiß jmd. warum das so ist?
Nachdem Du den neuen Thread gestartet hast, endet das Programm und damit auch alle Threads. Ohne `sleep()` hast Du Glück, dass anscheinend erst der neu gestartete Thread den Prozessor bekommt und genug Zeit hat komplett durchzulaufen. `sleep()` ist ein blockierender Aufruf, da wird sofort auf einen anderen Thread umgeschaltet - in diesem Fall der Hauptthread, der das Programm beendet.

In der Python-Konsole läuft als Hauptthread die Python-Shell/Eingabeschleife weiter.
spooky0815
User
Beiträge: 23
Registriert: Montag 27. Februar 2006, 19:24

Mittwoch 3. Januar 2007, 13:55

Hallo,
Nachdem Du den neuen Thread gestartet hast, endet das Programm und damit auch alle Threads. Ohne `sleep()` hast Du Glück, dass anscheinend erst der neu gestartete Thread den Prozessor bekommt und genug Zeit hat komplett durchzulaufen. `sleep()` ist ein blockierender Aufruf, da wird sofort auf einen anderen Thread umgeschaltet - in diesem Fall der Hauptthread, der das Programm beendet.
Stimmt...wieder etwas schlauer geworden :)


.. zurück zum Hauptproblem:

was hats mit der After-Methode auf sich...kann man anstatt der mainloop() methode nicht einfach ein regelmäßiges update() machen, welches dann gelockt wird?

VG und danke,

spooky0815
BlackJack

Mittwoch 3. Januar 2007, 15:25

spooky0815 hat geschrieben:was hats mit der After-Methode auf sich...kann man anstatt der mainloop() methode nicht einfach ein regelmäßiges update() machen, welches dann gelockt wird?
Die `after()`-Methode kann man benutzen, um eine Funktion nach einer bestimmten Zeit von Tkinter aus als "callback" aufrufen zu lassen.

Mit eigene `mainloop()` schreiben meinte ich das mit dem `update()`. Mit einem Lock müsste man da natürlich auch arbeiten können.
Antworten