GTK und Threads / Threading, Gobject !?

Programmierung für GNOME und GTK+, GUI-Erstellung mit Glade.
whaeva
User
Beiträge: 66
Registriert: Mittwoch 25. Februar 2009, 15:30

Liebe Leute,
ich versuche schon den halben Tag ein aktuelles Tutorial zu finden, das beispielhaft ein Programm mit GTK-GUI und Threads o.Ä. erklärt.
Und zwar aus dem Grund, dass ich schnellstmöglich was am Laufen haben möchte, ohne mich tiefgründig einzuarbeiten.
Die Tutorials, die ich gefunden habe, sind zum einen etwas veraltet, zum anderen werden die Zusammenhänge nicht weiter erklärt.

Der erste Reinfall ist ja schonmal, dass man Threads und GTK nicht ohne weiteres mischen kann, weil GTK natürlich selbst Threads benutzt.
(Anscheinend ist das Modul threads sowieso ein ausaufmodell und man sollte lieber mit threading eigene thread-klassen erstellen.)

Dann habe ich irgendwo gelesen, dass man sowieso besser Methoden aus gobjects benutzen sollte.

Ein Versuch mit gtk.gdk.threads_init() und gtk.threads_enter() bzw. gtk.threads_leave() führte ins Leere

Hier ist ein Snippet, wo es vermutlich richtig gemacht wird:
http://www.python-forum.de/topic-2711.html

[edit: auch schon veraltet:
GtkDeprecationWarning: gtk.threads_enter is deprecated, use gtk.gdk.threads_enter instead]


Hier ein ausführlicher Thread zu, Thema gobject
http://www.python-forum.de/topic-17662, ... ht=gobject

Hier noch ein paar Links:
http://www.pygtk.org/docs/pygtk/
http://docs.python.org/library/thread.html besser:
http://docs.python.org/library/threading.html
http://www.pygtk.org/docs/pygobject/

Hat zufällig jemand Lust die Allgemeinheit mit einem kleinen Tutorial zu bereichern?
Leonidas
Python-Forum Veteran
Beiträge: 16025
Registriert: Freitag 20. Juni 2003, 16:30
Kontaktdaten:

Die Frage ist: was willst du denn machen? Ich brauche in GTK-Applikationen quasi nie Threads, dafür reicht oftmals schon ``gobject.idle/timeout_add``.
My god, it's full of CARs! | Leonidasvoice vs (former) Modvoice
whaeva
User
Beiträge: 66
Registriert: Mittwoch 25. Februar 2009, 15:30

Hi,

Ziel ist ein Programm, das mehrere physikalische Größen über serielle Verbindungen steuert und ausliest, grafisch darstellt (evtl. matplotlib ?), die Daten bewertet und sowohl roh als auch ein Bericht evtl. als PDF auf Festplatte schreibt. (http://www.python-forum.de/topic-18055.html)

Die Kommunikation könnte man wahrscheinlich mit entsprechender Wartezeit in einer Schleife auch nacheinander machen.

Ich hatte allerdings angenommen, dass mit einem Thread vielleicht die Auslesefrequenz eines Gerätes maximiert werden kann, weil nicht auf anderes gewartet werden muss.
Es sollte nämlich eine Abbruchbedingung von einem Gerät so schnell wie möglich erfasst und daraufhin ein Befehl an ein anderes Gerät gesendet werden.

Bislang habe ich Klassen für die verschiedenen Geräte geschrieben, die die serielle Verbindung herstellen, und den Datenaustausch bzw. die Umwandlung von Werten durchführen. Knackpunkt ist da eigentlich nur die Prüfung auf Fehler (Nicht eingeschaltet oder falsch eingestellt -> timout der Verbindung -> Rückgabewert_Fehler = True), daher auch die Frage im anderen Thread. Die Behandlung dieses Problems ist wahrscheinlich mit "try: except:" eleganter als mit "if gerät.read()[3] dann nochmal"..

Wenn man auf Basis dieser Klassen ein Thread Objekt erstellen könnte, mit denen nebem dem Hauptthread auch die anderen Threads kommunizieren könnten, wär' das doch eine "gute" Lösung?

Jetzt möchte ich allerdings klein anfangen und erstmal eine einzelne Verbindung als Thread auslesen und anzeigen, mit dem genannten Codesnippet bin ich da mittlerweile auf einem funktionierenden Weg.

Wahrscheinlich sollte ich jedoch auf das Modul Threading umsteigen und die "Funktion" als Klasse schreiben.

Also, falls du mein Vorhaben durchschaust, würde ich mich über Tipps freuen!
whaeva
User
Beiträge: 66
Registriert: Mittwoch 25. Februar 2009, 15:30

Leider stoße ich selbst bei dem Minimalprogramm an meine Grenzen.
Es startet einen Thread und läuft erwartungsgemäß, allerdings wird der Thread nicht beendet und auch sonst weiß ich nicht, ob das so im Sinne des Erfinders ist..
Hilfe!?

Code: Alles auswählen

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys
import threading
import Queue
import types
import time
import gtk				# GTK+ 2


class mySerialDevice(object):
	
	def __init__(self, port):
		self.port = port
	
	def connect(self):
		pass
		#~ try:
			#~ self.serial = serial.Serial(	port=self.port,
											#~ baudrate=9600,
											#~ bytesize=serial.EIGHTBITS,
											#~ parity=serial.PARITY_NONE,
											#~ stopbits=serial.STOPBITS_ONE,
											#~ timeout = 2
										#~ )
		#~ except serial.SerialException:
			#~ exit("Cannot open serial port")

	def disconnect(self):
		pass
		#~ self.serial.close()


class myThread(threading.Thread):

	def __init__(self, label):
		"""
		"""
		threading.Thread.__init__(self)
		self.label = label
		self.counter = 0
		self.dev = mySerialDevice("/dev/ttyS0")
		self.dev.connect()


	def run(self):
		"""
		"""
		
		while True:
			self.counter += 1
			time.sleep(1)
			gtk.gdk.threads_enter()
			self.label.set_text("%d" % self.counter)
			gtk.gdk.threads_leave()
			
	def stop(self):
		self.dev.disconnect()
		self.kill() # WIE?
		


class myGUI:
	"""
	"""

	def __init__(self):
		builder = gtk.Builder()
		builder.add_from_file("test.xml") 
		builder.connect_signals(self)
		
		self.window = builder.get_object("window")
		self.label = builder.get_object("label")
		
		self.test = myThread(self.label)
		self.test.start()
		
		self.counter = 0


	def run(self):
		pass


	def main(self):
		gtk.main()


	def on_window_destroy(self, widget, data=None):
		self.test.stop()
		gtk.main_quit()


	def on_button_clicked(self, widget, data=None):
		self.label.set_text("button_clicked")





if __name__ == "__main__":
	gtk.gdk.threads_init()

	app = myGUI()
	gtk.gdk.threads_enter()
	app.main()
	gtk.gdk.threads_leave()
Hier das mit

Code: Alles auswählen

gtk-builder-convert test.glade test.xml
für gkt.builder() konvertierte XML:

Code: Alles auswählen

<?xml version="1.0"?>
<!--Generated with glade3 3.4.5 on Sat Mar  7 12:31:49 2009 -->
<interface>
  <object class="GtkWindow" id="window">
    <property name="visible">True</property>
    <property name="title" translatable="yes">Window Title</property>
    <signal handler="on_window_destroy" name="destroy"/>
    <child>
      <object class="GtkVBox" id="vbox1">
        <property name="visible">True</property>
        <child>
          <object class="GtkLabel" id="label">
            <property name="visible">True</property>
            <property name="label" translatable="yes">label</property>
          </object>
        </child>
        <child>
          <object class="GtkButton" id="button">
            <property name="visible">True</property>
            <property name="can_focus">True</property>
            <property name="receives_default">True</property>
            <property name="label" translatable="yes">button</property>
            <signal handler="on_button_clicked" name="clicked"/>
          </object>
          <packing>
            <property name="position">1</property>
          </packing>
        </child>
      </object>
    </child>
  </object>
</interface>
whaeva
User
Beiträge: 66
Registriert: Mittwoch 25. Februar 2009, 15:30

Nach einem sinnlosen Versuch mit threading.event liest man bei
http://docs.python.org/library/threading.html#id1
Once the thread’s activity is started, the thread is considered ‘alive’. It stops being alive when its run() method terminates – either normally, or by raising an unhandled exception. The is_alive() method tests whether the thread is alive.
Die endlose Whileschleife in myThread.run() braucht also eine Abbruchbedingung, damit der Thread beendet werden kann.

Trotzdem würde mich interessieren, ob das so der richtige Weg ist.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

Ein Code Bsp. findest Du unter http://www.python-forum.de/topic-18028.html 2. Post. Hier siehst Du, wie Du die Abbruchbedingung in Dein while packst. Da Du mit einem Klassenansatz fährst, müßtest Du das ein wenig umstricken (Event-Initialisierung in Konstruktor, set() anstelle Deines kills, while wie gehabt usw.)
whaeva
User
Beiträge: 66
Registriert: Mittwoch 25. Februar 2009, 15:30

Schon ergibt sich die nächste Frage:

Im Beispiel bekommt myThread ein Handle zum Zugriff auf das Widget label.
Im echten Programm sind allerdings noch weitere, abhängige Labels zu ändern, myThread soll auf Anweisung vom GUI auch noch andere Funktionen ausführen, usw.

Wie entscheide ich, was in den Thread, und was in die GUI gehört?
Lässt sich der Datenaustausch zwischen Thread und GUI irgendwie allgemeiner durchführen?
whaeva
User
Beiträge: 66
Registriert: Mittwoch 25. Februar 2009, 15:30

@ jerch:
Danke, den Schleifenabbruch habe ich bislang mit einer Variable gelöst, aber mit signal geht's natürlich auch - was ist der Vorteil vom signal (abgesehen von der wait funktion)?
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

Wie entscheide ich, was in den Thread, und was in die GUI gehört?
Lässt sich der Datenaustausch zwischen Thread und GUI irgendwie allgemeiner durchführen?
Hm, das läßt sich nicht so einfach beantworten. Für Multithreading-Anwendungen gehört alles gui-spezifische definitiv in die "Gui-Klasse", sollte also gui-nah behandelt werden. Die Komunikation würde ich dann über signals realisieren, so können mehrere konkurrierende Threads ohne lästige Locks weiterarbeiten (was halt passiert, wenn ein zweiter Thread versucht, einen Mutex zu erhalten während ein anderer ihn noch hält). Die Signale würden dann einfach von event-handler der Gui nacheinander abgearbeitet.
Den Weg, den Du im Moment gehts, holt sich den Mutex (threads_enter()), beschreibt eine "globale" Variable (Dein Label) und gibt den Mutex wieder frei (threads_leave()). Das ist vollkommen legitim, könnte aber bei mehreren Threads halt zu den beschrieben Locks führen (die Threads müßten dann unnötig angehalten werden). Für Deine Anwendung würde das meiner Meinung nach in Ordnung gehen, da Du myThread nur einmal anwirfst. Trotzdem würde ich eher den Signalweg bevorzugen. Dann hättest Du auch eine bessere Trennung von Gui-stuff und Thread-Logik.
whaeva
User
Beiträge: 66
Registriert: Mittwoch 25. Februar 2009, 15:30

"Hi" und danke für deine Unterstützung.
Ich bin leider nicht sicher, dass ich dich verstanden habe:

myThread macht sein Ding, bekommt ein Ergebnis, schreibt das in eine echte, globale Variable und setzt ein Signal.

myGUI (=gtk) wartet auf dieses Signal (wie?) und aktualisiert dann "label" mit dem Datum aus der globalen Variable.

So? Dann müsste ich ja GTK noch beibringen, auf das Signal zu reagieren.

In irgendeiner Newsgroup habe ich eben auch etwas davon gelesen, ein globales Dictionary anzulegen, dass dann für den Datenaustausch dient. Ist das der "beste" Weg? Geht das nicht in Richtung Queue?

http://docs.python.org/library/queue.html
whaeva
User
Beiträge: 66
Registriert: Mittwoch 25. Februar 2009, 15:30

Habe ein sehr gutes Tutorial zum Thema Threading und Queues gefunden:
http://www.ibm.com/developerworks/aix/l ... index.html

Queues sind wohl nicht Lösung für mein Problem.
Ich suche wohl eher den Zusammenhang zwischen einem "eigenen" Event und dem builder.connect_signals(self).
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

Hui, ich sagst mal so, viele Wege führen nach Rom.
Ich bin leider nicht sicher, dass ich dich verstanden habe:

myThread macht sein Ding, bekommt ein Ergebnis, schreibt das in eine echte, globale Variable und setzt ein Signal.
Naja nicht so ganz, ich meinte eher -

Entweger:
Globaler Zugriff, wie Du es mit dem Label schon machst, nur sinnvoll für eins/zwei direkte Gui-"Eingriffe", sonst hast Du zuviel Gui-Code im Thread, wie selbst schon bemerkt hast. Für wenige Manipulationen fänd ich das noch azeptabel, andere würden Dir sofort schlechten Programmierstil vorwerfen und mit zig Programmierparadigmen wedeln. ;) Eine gute Trennung zw. Thread- und Guilogik hättest Du so halt nicht, was definitiv auf Kosten der Wartbarkeit geht.

Oder:
Du nutzt Signale. Das tolle Gui-Libraries wie Gtk, Qt ist doch, das das Eventhandling Dir quasi geschenkt wird und mit Signal/Slot-Konzepten die Kommunikation zw. verschiedenen Objekten sehr einfach wird. Unten findest Du ein grobes Schema für PyGTK, wie man das in Deinem Falle nutzen könnte.
In irgendeiner Newsgroup habe ich eben auch etwas davon gelesen, ein globales Dictionary anzulegen, dass dann für den Datenaustausch dient. Ist das der "beste" Weg? Geht das nicht in Richtung Queue?
Auch hiermit würdest Du zum Ziel kommen, müßtest Dich aber selbst um das Eventdispatching kümmern. Da Deine Grundbibliotheken aber schon alles mitbringen, ist das viel zu umständlich. (siehe oben Oder:)

Editiere Deinen Code doch mal wie folgt:

Code: Alles auswählen

import gobject
...
class myThread(threading.Thread, gobject.GObject):
	# signale deklarieren
	__gsignals__ =  {"update":(gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, [gobject.TYPE_INT])}
	def __init__(self, label):
		...
		gobject.GObject.__init__(self)

	def run(self):
		...
			# in der while-Schleife anstelle der
			# gtk.gdk.threads_enter()
			# self.label.set_text("%d" % self.counter)
			# gtk.gdk.threads_leave()
			self.emit("update", self.counter) # signal "update" absetzen

class myGUI:
	def __init__(self):
		...
		self.test = myThread(self.label)
		# signal test.update mit updateLabel verbinden
		self.test.connect("update", self.updateLabel)
		self.test.start()
...
	def updateLabel(self, widget, num):
		self.label.set_text("%d" % num)
Mit der zusätzlichen Ableitung von GObject erhält MyThread einige Basisfunktionalitäten der Gtk-Objekte, unter anderen kann MyThread jetzt Signale absetzen, die Deine Gui-Klasse versteht. Dadurch kannst Du mit MyThread wie mit jedem anderen Gtk-Objekt (genauer GObject) via Signal/Slot reden. Großer Vorteil, die Signale sind Events die normal via connect() verbunden und vom eingebauten Eventhandling automatisch abgearbeitet werden.

Grüße, Jerch
whaeva
User
Beiträge: 66
Registriert: Mittwoch 25. Februar 2009, 15:30

Perfekt, danke für die Erläuterung. Ich probiere das jetzt mal aus.
Benutzeravatar
Trundle
User
Beiträge: 591
Registriert: Dienstag 3. Juli 2007, 16:45

Vorsicht: Man darf grundsätzlich nicht mit mehreren Threads auf die GUI zugreifen. Die Signal-Handler werden jedoch im gleichen Thread aufgerufen, in dem das Signal ausgelöst wurde. Das bedeutet also, dass Signal-Handler des "update"-Signals des ``myThread``-Objekts nicht direkt auf die GUI zugreifen dürfen.

Außerdem ist noch anzumerken, dass `gtk.gdk.threads_enter()`/`gtk.gdk.threads_leaver()` unter Windows nicht zuverlässig funktioniert, da man nur von einem Thread aus (und nicht einfach nur nicht mit mehreren gleichzeitig) auf GDK-Funktionen zugreifen sollte, also sollte man lieber `gobject.idle_add` verwenden (siehe auch "Functions for using GDK in multi-threaded programs", Abschnitt "Description").
"Der Dumme erwartet viel. Der Denkende sagt wenig." ("Herr Keuner" -- Bertolt Brecht)
whaeva
User
Beiträge: 66
Registriert: Mittwoch 25. Februar 2009, 15:30

Also das Programm läuft jetzt schon ein bisschen mehr :-)
Wenn es unter Windows nicht liefe, ist das für mich nicht schlimm.

Zu deinem 2. Satz: Ich bin der Meinung, dass dem nicht so ist. Das Signal/Event wird in "MyThread" emitted, und in myGUI ist der Handler. ?

Unabhängig davon habe ich noch eine Frage:
In "myGUI" ist "label" durch "updateLabel" aktualisiert worden.
Den übermittelten Wert kann ich in updateLabel ja auch noch für meine anderen Zwecke Nutzen. Gibt es theoretisch auch die Möglichkeit, durch self.label.set_markup() ein Event auszulösen?
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@Tundle:

Das mit dem Gui-Zugriff aus nur einem Thread heraus verstehe ich anders. Vielmehr scheint der zeitgleiche Zugriff über mehrere Threads nicht sicher zu sein, was mit threads_enter/leave eben abgesichert werden soll (also prinzipiell möglich ist).
Siehe hierzu: http://research.operationaldynamics.com ... eness.html
und http://research.operationaldynamics.com ... -java.html

(Von Qt her war ich davon ausgegangen, daß signal-emitting thread-safe ist, was in Gtk eben nicht der Fall ist.)

Ich bin mir des Windows-Problemes bewußt, nur spielt Gtk auf Windows für mich keine Rolle. Trotzdem hab ich mal das Script angepaßt und hätte gern mal eine Stellungnahme von Dir hierzu.

Code

Das Signal wird jetzt per idle_add() abgesetzt, ein direkter Threadzugriff erfolgt nicht mehr. Verbesserungsvorschläge?

Grüße, Jerch
Benutzeravatar
Trundle
User
Beiträge: 591
Registriert: Dienstag 3. Juli 2007, 16:45

@whaeva:
Wo das im Code steht, sagt doch rein gar nichts darüber aus, in welchem Thread das ausgeführt wird. Der Code hier wirft einen `AssertionError`, was er aber nicht tun dürfte, wenn der Handler im gleichen Thread aufgerufen würde, in dem auch die Mainloop läuft.

@jerch:
Das kommt eben auf die Plattform an. Unter X11 darf man mit mehreren Threads zugreifen, nur eben nicht gleichzeitig. Unter Windows darf man aber wohl überhaupt nicht mit mehreren Threads zugreifen. Ob man Windows unterstützen mag oder nicht, muss jeder mit sich selbst ausmachen. Ich persönlich mag jedoch schon, dass meine Programme unter Windows laufen und habe auch keine Lust, versehentlich einen Deadlock einzubauen, weshalb ich `idle_add()` benutze.

Zum Code: Sieht bis auf die ``on_window_destroy``-Methode gut aus. In `on_window_destroy()` ist jedoch die Hauptschleife von GTK+ unterbrochen und es wird gewartet, bis der Thread beendet wird, dh in der Zeit, in der der Thread noch lebt, wird die GUI nicht neu gezeichnet und das Fenster verschwindet auch nicht, wodurch sich die GUI eben "unschön" anfühlen könnte, da sie nicht gleich reagiert. Und das ``data=None`` in den Handlern ist auch überflüssig und eher irreführend. In C wird den Handlern immer noch ein Zeiger mit Userdaten übergeben (der auch "NULL" sein kann), in Python können aber noch beliebig viele Argumente folgen, nämlich die, die man beim Verbinden des Signals angegeben hat. Hat man da keine angegeben, wird auch kein weiteres übergeben.
"Der Dumme erwartet viel. Der Denkende sagt wenig." ("Herr Keuner" -- Bertolt Brecht)
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

Danke Trundle.
Der Kritik an dem data=None nehm ich mich mal nicht an, war nur copy&paste von whaeva (zugegebenermaßen ohne Verstand lol).

ok join hinter main_quit, vllt. mit timeout. Wenn der join hier nicht mehr gelingt, liegt eh ein problem mit dem thread vor :(, dann kann nur noch das OS helfen.
ms4py
User
Beiträge: 1178
Registriert: Montag 19. Januar 2009, 09:37

Ich habe die Diskussion jetzt nur überflogen, aber hier noch meine persönliche Erfahrung zum Thema GTK und Threads unter Windows.

Threads und GTK funktionieren auch unter Windows problemlos, solange man nicht aus mehreren Threads auf die GUI Elemente zugreifen will. Und das willst du ja auch nicht, soweit ich das mitbekommen habe.

Um Threads im GTK Mainloop nutzen zu können, reicht ein

Code: Alles auswählen

gobject.threads_init()
Da muss man dann auch nicht mit dem thread_enter und thread_leave arbeiten, sondern kann die Standard Threads mit den Standard Lock Funktionen nutzen, falls man in kritische Bereiche kommt.

Hier ist ein Beispiel (allerdings funktioniert die Loggerklasse so nicht)
http://www.python-forum.de/topic-17844.html
Benutzeravatar
Trundle
User
Beiträge: 591
Registriert: Dienstag 3. Juli 2007, 16:45

Manchmal ist es hilfreich, Diskussionen nicht nur zu überfliegen. Es gibt die Dokumentation von GDK, die klar sagt, dass man unter Windows besser nicht versuchen sollte, von mehreren Threads aus auf GDK-Funktionen zuzugreifen. Und zwar grundsätzlich, nicht nur nicht gleichzeitig (was hier auch von Owen Taylor bestätigt wird: "Using GTK+ from multiple threads on Windows doesn't work at all, and would require substantial work and complexity to fix"). Das gefährliche an Threads und GUI ist, dass Fehler nicht deterministisch auftreten, wodurch das mit der eigenen praktischen Erfahrung immer so eine Sache ist.

Desweiteren ist mir nicht klar, wie du nur mit eigenen Locks verhindern willst, dass nicht gleichzeitig auf die GUI zugegriffen wird. Damit verhinderst du, dass dein eigener Code in den Threads vllt nicht gleichzeitig auf die GUI zugreift. Aber dabei wird doch die Mainloop von GTK+ nicht unterbrochen. Das heißt, es wird auf Events reagiert, Signale werden ausgelöst, etc. pp. Und jetzt soll auf einmal GTK+ nicht mehr von sich aus auf die GUI zugreifen, nur weil du einen eigenen Lock hast, von dem GTK+ überhaupt nichts weiß?
"Der Dumme erwartet viel. Der Denkende sagt wenig." ("Herr Keuner" -- Bertolt Brecht)
Antworten