Wie sicher ist os.pipe() zum Triggern in der tk Task?

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

Wenn wir etwas von einem anderen Thread in die tk mainloop übermitteln wollen, gibt es keine optimalen Lösungen, die sicher sind.

after: sicher für pollen in der mainloop, aber nicht optimal
after: ohne pollen von einer anderen Task aus funktioniert bei manchen python und tkinter Versionen aber nicht bei allen
event_generate: sehr gut, wo es funktioniert, leider funktioniert es auch nicht bei allen python und tkinter Versionen
os.pipe: würde gerne Eure Meinung dazu wissen

Code: Alles auswählen

def trigger():
    print("define here your intern trigger for your tkinter app")

import os
pipe_read, pipe_write = os.pipe() ;
pipe_write = os.fdopen(pipe_write, 'w')

# trigger for the extern thread (if you have more than one other thread then use also a lock)
def trigger_extern():
    pipe_write.write('x')
    pipe_write.flush()

# triggered in your app by trigger_extern
def triggered_by_extern(*args):
    os.read(pipe_read,1)
    trigger()

root.tk.createfilehandler(pipe_read, tk.READABLE, triggered_by_extern)
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Also, ich weiß auch nicht, ob die pipe gut ist. Da bin ich eher etwas skeptisch. Ich hatte dort ein Beispiel mit mehreren Tasks vorgestellt: http://www.python-forum.de/viewtopic.ph ... &start=120

Zu empfehlen wäre hier wohl pollen für Messages von externen Tasks:

Code: Alles auswählen

# update external messages
def external_update():
    proxy.do_work()
    root.after(100,external_update)

external_update()
Wenn man externe Tasks hat und es auf Geschwindigkeit der Kommunikation mit externen Tasks ankommt, dann sollte man auch die Geschäftslogik in eine externe Task verlegen. Für GUI Updates sollten 100 ms (eine Zehntelsekunde) dann völlig ausreichen.

Eine Frage: ich habe gelesen, man pollt mit after und after_idle. Das mit after_idle habe ich aber noch nicht verstanden, warum man das macht. Kann jemand dazu etwas schreiben?
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@Alfons Mittelmeyer:
Unter POSIX ist das Lesen und Schreiben bis zu PIPE_BUF zwingend atomar und damit auch threadsafe. In einer mutlthreaded Applikation ist mir das allerdings noch nie als Mutex- oder Lockersatz für den Datenaustausch untergekommen. Ich halte es auch für unpraktisch, da nur serialisierte Daten durchpassen und man unnötige Kernel-/Userlandkontextwechsel einführt.
BlackJack

@jerch: Die Daten gehen ja gar nicht durch die Pipe, die dient nur der Signalisierung das es Daten gibt, die programmintern dann beispielsweise aus einer Warteschlange geholt werden können.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

@jerch, ist klar, dass das in einer sonstigen multithreaded Anwendung nicht vorkommt. Da ist die Schwäche von Python, dass in threading nichts Brauchbares geboten wird, außer sperrenden Funktionen und nichts Zusätzliches, wie etwa ein event, der dann im tk Thread ausgeführt wird. Und auch die threading.timer Funktion ist ein extra Thread. Man kommt also über Python nicht in den tk Thread hinein, wenn man nicht tkinter Events aussperren will - und das wäre gewiß nicht wünschenswert. Und tkinter bietet leider auch nichts Sicheres. Aber unpraktisch ist so eine Pipe zum Datenaustausch nicht, denn Daten auf eine Queue zu legen ist ja kein Problem. Es geht nur darum, die tkinter Mainloop aufzuwecken und die Ausführung einer Funktion - immer dieselbe - zu triggern. Naja diese 'unnötigen Kernel-/Userlandkontextwechsel' brauchen bestimmt keine 100 ms, was ich für einen angebrachten Wert für Pollen ansehen würde.

Man könnte natürlich noch mehr untersuchen, etwa läuft tkinter in einem QT Thread, sodass man mit einem QT Timer etwas machen könnte? Aber ich glaube, wenn man so weit geht, sollte man gleich auf QT umsteigen - und dort haben wir ja diese signal Funktionen für GUI Aufrufe von anderen Threads.

Habe noch eine andere Fragestellung untersucht: Sind die tkinter events zusätzliche Events, die auch stattfinden, wenn man ein event.wait() in der Mainloop hat. Nö, da geht dann gar nichts mehr. Man kann dann auch nichts mehr in ein Entry Feld eingeben. Kein Cursor, kein Nichts. Also keine sichere Lösung außer Pollen und evtl. pipe()

Man hätte doch das in einer extra Task imlementieren können und nur die Event Callbacks dann in der Mainloop ausführen. Aber nicht weit genug nachgedacht.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Hab es nochmals für Multitasking mit Lock und readline sieht für manche besser aus als os.read:

Code: Alles auswählen

import os
import threading

pipe_read, pipe_write = os.pipe() ;
pipe_write = os.fdopen(pipe_write, 'w')
pipe_read = os.fdopen(pipe_read, 'r')

lock = threading.Lock()

# trigger for the extern thread
def trigger_extern():
    lock.acquire()
    pipe_write.write('\n')
    pipe_write.flush()
    lock.release()
 
# triggered in your app by trigger_extern
def triggered_by_extern(*args):
    pipe_read.readline()
    proxy.trigger()

proxy.extern_trigger = trigger_extern 
root.createfilehandler(pipe_read, tk.READABLE, triggered_by_extern)

root.mainloop()
pipe_write.close()
pipe_read.close()
Sirius3
User
Beiträge: 17741
Registriert: Sonntag 21. Oktober 2012, 17:20

Erstens müßte es `with lock:` heißen, zweitens ist schreiben in eine Pipe atomar und drittens wird da ja nur ein Aufweckzeichen geschrieben. Also das Lock ist absolut überflüssig.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@Alfons:
Irgendwie grenzt das an Selbstgeiselung - wenn weder Tkinter noch Tcl selbst Threadsicherheit garantieren können, wirst Du die Problematik auf Pythonebene nicht zufriedenstellend lösen können. Weiter oben hast Du was von QTimer geschrieben - das macht im Zusammenhang im Tkinter auch keinen Sinn, warum dann nicht gleich auf Qt auch als GUI-Toolkit setzen, was das bereits zufriedenstellend umsetzt?
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

jerch hat geschrieben:@Alfons:
Irgendwie grenzt das an Selbstgeiselung - wenn weder Tkinter noch Tcl selbst Threadsicherheit garantieren können, wirst Du die Problematik auf Pythonebene nicht zufriedenstellend lösen können. Weiter oben hast Du was von QTimer geschrieben - das macht im Zusammenhang im Tkinter auch keinen Sinn, warum dann nicht gleich auf Qt auch als GUI-Toolkit setzen, was das bereits zufriedenstellend umsetzt?
Also, bitte warum, liest Du meine Postings nicht genau, denn genau das hatte ich geschrieben, dass man da gleich auf Qt umsteigen sollte:
Man könnte natürlich noch mehr untersuchen, etwa läuft tkinter in einem QT Thread, sodass man mit einem QT Timer etwas machen könnte? Aber ich glaube, wenn man so weit geht, sollte man gleich auf QT umsteigen - und dort haben wir ja diese signal Funktionen für GUI Aufrufe von anderen Threads.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

Warum bist Du noch auf Tkinter und Pipes? :twisted:
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

@jerch Es wäre noch interessant, Nachrichten mit anderen Anwendungen zu tauschen. Aber alle Betriebssysteme haben wir wohl nichts Gleiches zur Zeit, oder?
Für Unix gibt es die named FIFO pipe. Aber für windows muss man dann so etwas wie win32pipe nehme oder auch win64pipe? Wieso kann man da nicht alles gleich machen?
BlackJack

@Alfons Mittelmeyer: Wenn man alles gleich macht dann gibt's irgendwie keine unterschiedlichen Betriebssysteme. ;-)
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

BlackJack hat geschrieben:@Alfons Mittelmeyer: Wenn man alles gleich macht dann gibt's irgendwie keine unterschiedlichen Betriebssysteme. ;-)
Ja warum wird dann nichts unahängiges gemacht? Genau das würde man brauchen, aber auch für Windows
os.mkfifo(path[, mode])

Create a FIFO (a named pipe) named path with numeric mode mode. The default mode is 0666 (octal). The current umask value is first masked out from the mode.

Availability: Unix.

FIFOs are pipes that can be accessed like regular files. FIFOs exist until they are deleted (for example with os.unlink()). Generally, FIFOs are used as rendezvous between “client” and “server” type processes: the server opens the FIFO for reading, and the client opens it for writing. Note that mkfifo() doesn’t open the FIFO — it just creates the rendezvous point.
Quelle: https://docs.python.org/2/library/os.html

Und so etwas ist grauenhaft. Würde ich nicht ernsthaft implementieren:

Code: Alles auswählen

import os

pipe_write=open("pipe_read",'w')
while True:
    a = input(">")
    pipe_write.write(a)
    pipe_write.flush()

Code: Alles auswählen

import os

pipe_read=open("pipe_read",'r')
while True: 
    a = pipe_read.read()
    if len(a) != 0: print(a)
Hatte es mit readline probiert, aber das wartet auch nicht. Naja, den File vollmüllen ist ja so eine Art Logfile, oder?

Das wären dann zwei verschiedene Anwendungen.

Man könnte es natürlich auch mit zwei Files machen. Die Anwendung, die liest, liest den File und löscht ihn danach. Die Anwendung, die schreibt, schreibt in einen anderen File und wenn der este gelöscht ist, schließt sie ihn, benennt in um und öffnet dann wieder einen zum Schreiben.

Das wären dann vier Files für eine beidseitige Kommunikation. Gefällt mir auch nicht besonders. Aber JSON könnte man dabei verwenden. Wenn drei Anwendungen über eine vierte miteinander kommunizieren, wären das 12 Files. Da wäre dann eher TCP/IP zu überlegen.

JSON über TCP/IP zu anderen Anwendungen, gibt es hierfür etwas?

httplib geht über http Server. Gibt es auch etwas anderes zur Kommunikation auf demselben System?

Man könnte ja über localhost gehen: https://docs.python.org/3/library/socketserver.html
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Hab es mit UDP Server gemacht, zumindest mal von einem UDP Client zur GUI und die ist mit einem UDP Server verbunden. Siehe hier: http://www.python-forum.de/viewtopic.php?f=3&t=36933
Benutzeravatar
sparrow
User
Beiträge: 4187
Registriert: Freitag 17. April 2009, 10:28

Für das hier Beschriebene (also Datenaustausch mi JSON-Format) ist UDP das falsche Protokoll. TCP drängt sich da eher auf, weil das die stetige Verbindung und die Garantie der vollständigen Übertragung bereits mitbringt. Ansonsten müsste man das mühevoll in sein Programm basteln oder mit fehlenden Teilen der Übertragung leben... bzw. das Programm sterben.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

@sparrow Ja, da hast Du wohl recht. Aber trotzdem werd ich nicht jetzt noch damit anfangen statt UDP TCP zu implementieren. Da wäre noch etwas zusätzlicher Aufwand nötig, weil sich der Router dann auch noch die Connections merken muss. Viel wäre ja nicht zu tun. Aber genaugenommen, brauche ich das ja alles nicht. Wollte nur sehen, zu was mein Proxy taugt.

Ein zsätzliches boolsches Flag bei der Definition eines Callbacks erlaubte, dass ein Callback nicht nur die Message erhielt, sondern auch bei Bedarf die Message ID. Damit war dann Routing in andere Threads möglich.
Dass man nicht nur einen boolschen Wert nehmen kann, sondern auch etwas Beliebiges, erlaubt, dass der Callback zusätzlich noch einen weiteren Parameter bekommt. Das kann das alles Mögliche sein, etwa eine Port Adresse oder eine IP Adresse. Damit ist dann Routing über Netzwerke möglich.

Ich denke für Routing zwischen Anwendungen auf dem eigenen PC über localhost sollte wohl UDP reichen. Da sollte normalerweise kein Bit umkippen und somit kein Paket unbrauchbar werden. Wenn man vor hat, über ein Netzwerk zu gehen, sollte man TCP verwenden.

Und ob man dann dafür etwas Eigenes verwenden sollte, bin ich skeptisch. Lieber in so einem Fall wohl dann etwas Erprobtes und Bewährtes nehmen und das dann noch mit anbinden, entweder an die Applikation über ein modifiziertes Sende und Empfangsteil oder als eigenständige Anwendung entweder als modifizierter Router oder über den UDP Router - im Falle mehrer miteinander kommunizierender Anwendungen.

Aber damit sind wir jetzt ganz vom Thema abgekommen. Naja, os.pipe sollte wohl sicher sein als Trigger für Message Austausch der tk Anwendung mit anderen Threads. Ob man den Aufwand betreiben will ein paar Zeilen mehr Code zu schreiben, oder einfach mit after pollt das kann man sich aussuchen.

Und auch damit bin ich von meinem eigenen Anliegen abgekommen, nämlich dem GUI Designer. Und dafür brauche ich den Message Austausch zwischen verschiedenen Threads auch nicht. Habe nur meinen Proxy für threadinterne Messages geringfügig erweitert, so dass er auch für Messageaustausch überall hin auch über über Netzwerke taugt - und habe es mal ausprobiert.
Benutzeravatar
snafu
User
Beiträge: 6738
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Alfons Mittelmeyer hat geschrieben:Ich denke für Routing zwischen Anwendungen auf dem eigenen PC über localhost sollte wohl UDP reichen. Da sollte normalerweise kein Bit umkippen und somit kein Paket unbrauchbar werden. Wenn man vor hat, über ein Netzwerk zu gehen, sollte man TCP verwenden.
Eine Verbindung über `localhost` stellt genau so ein Netzwerk dar. Nur eben eines auf engstem Raum.

Deine Folgerung, die sich für mich so liest, als solle man für geringe Übertragungswege bevorzugt UDP verwenden, weil da ja angeblich nicht viel schief gehen kann, halte ich für sehr gewagt. Man möchte IMHO grundsätzlich TCP verwenden und UDP nur dann nutzen, wenn gute Gründe gegen TCP sprechen. Und nein: Die Tatsache, dass du anscheinend nicht weißt, wie man Polling (oder ähnliche Techniken) mit einem TCP-Client betreibt, ist für mich definitiv kein sinnvoller Grund, stattdessen auf UDP zu setzen.

Aber: Es ist dein Programm (oder Theorie). Mach, was du willst. Machste ja eh. :)
Benutzeravatar
snafu
User
Beiträge: 6738
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Übrigens wird hier mit viel Beispielcode unter anderem beschrieben, wie man die plattformübergreifende `select()`-Funktion aus dem gleichnamigen Modul der Standardbibliothek nutzen kann. Man muss schon ein wenig Aufwand betreiben, aber hat dafür am Ende eine robuste Implementierung, wenn man alles richtig gemacht hat.

Bei Interesse einfach mal im Netz schauen, ob diesbezüglich vielleicht schon jemand etwas vorgefertigtes als Bibliothek bereitstellt, sodass man nicht soviel eigenen Code dafür schreiben muss.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

@snafu Hab es eh schon mit TCP gemacht und sicherer, schneller und cleverer als in solchen Beispielen. select geht ewa unter Unix, aber ist nicht für Windows.
Ich benutze einen Router mit ThreadingMixIn und die Clients des Routers haben auch einen Server. Hier der Server meines Routers:

Code: Alles auswählen

HOST, PORT = "localhost", 9999

class MyTCPHandler(socketserver.StreamRequestHandler):

    def handle(self):
        # messages, which requires its socket as parameter ================
        commands = {ECHO:False,ID_REQUEST:True,REGISTER:True,DISCONNECT:True}

        # client connects and sends its ip and port address of its server ================
        message = str(self.rfile.readline().strip(), "utf-8")
        json_message = json.loads(message)
        receiver_address=(json_message[0],json_message[1])

        # connect to clients server and send an info ================
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.connect(receiver_address)
        worker.proxy.send_extern_highprio(INFO,(sock,"Router Connected with: " + str(receiver_address)))

        # further messages of the client =====================
        while True:       

            message = self.rfile.readline()
            if len(message) == 0: break
            
            message_id = message[0:4] # first 4 bytes are the message id

            # for router command messages we need the socket as a parameter for responding to the client or to register or unregister message ids for the client
            if message_id in commands:
                if commands[message_id]: worker.proxy.send_extern_highprio(message_id,(sock,message))
                else: worker.proxy.send_extern(message_id,(sock,message))

            # socket parameter not required: registered messages are sent to the clients, which did register for message ids
            else: worker.proxy.send_extern(message_id,message)
            
        # if the client ends the session, we unregister all its message ids and close the socket
        worker.proxy.send_extern_highprio(DISCONNECT,(sock,None))
        print("Router Connection closed:",receiver_address) 
       
class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
    allow_reuse_address = True

server = ThreadedTCPServer((HOST, PORT), MyTCPHandler)
server.serve_forever()
Wenn ein Client sich verbindet, sendet er zuerst die IP und Port Adresse seines Servers. Daraufhin wird eine Socket Verbindung mit seinem Server eröffnet.
Die Kommunikation erfolgt dann ohne Warten auf Response und daher schnell.
BlackJack

@Alfons Mittelmeyer: `select()` funktioniert auch bei Windows für Sockets.
Antworten