GUI parallel zu GNURadio, Übergabe von Informationen

Plattformunabhängige GUIs mit wxWidgets.
Antworten
Alex7713
User
Beiträge: 8
Registriert: Mittwoch 9. Oktober 2013, 14:17

Hallo Python-Forum-Mitglieder,

ich arbeite z.Z. an einem GNURadio-Projekt und muss eine GUI in Python schreiben, da ich Neuling bin, bin ich auf zwei Probleme gestoßen.

Das Projekt geht wie folgt:

Es gibt vier Threads, die in unterschiedlichen Klassen definiert wurden, drei für GNURadio, die in einer Endlosschleife nacheinander ablaufen und einen, in dem die GUI gestartet wird und immer parallel läuft.

In der GUI können Elemente betätigt werden, jedes von ihnen verändert in der Event-Funktion eine globale Variable. Sobald in der GUI ein spezieller Button gedrückt wird, werden alle Parameter gelesen und ALLE IN EINEM RUTSCH nach GNURadio übernommen. Die wird in der Eventfunktion dieses speziellen Buttons gemacht.

1) Wie kann ich sicherstellen, dass ALLE Parameter in der Eventfunktion übernommen werden und der Thread-Sheduler nicht nach der Hälfte unterbricht und einen anderen Thread startet? Dann wäre die Hälfte nicht nach GNURadio übernommen worden.

2) Ich suche nach einer Möglichkeit, dass Code in Python nicht unterbrochen werden darf. Sobald eine Zeile Code von einem Thread betreten wird, darf der ausführende Thread nicht unterbrochen werden. (Locken würde nur dafür sorgen, dass die selben Zeilen nicht von einem anderen Thread gestartet werden dürfen, aber die spezielle Eventfunktion wird ja nur vom GUI-Thread genutzt).

Ich hoffe, ich konnte das Problem verständlich machen.

Vielen Dank,

Alex
BlackJack

@Alex7713: Irgendwie sind 1) und 2) das selbe oder? Wobei ich das Problem bei 1) nicht verstehe und 2) kann man nicht machen würde ich sagen. Wobei ich das Grundproblem irgendwie auch nicht so ganz verstehe.

Die Beschreibung mit den vier Threads und drei Klassen die in einer Endlossschleife nacheinander ablaufen + GUI habe ich nicht so ganz verstanden. Wenn die Klassen in *einer* Endlosschleife *nacheinander* ablaufen, dann sehe ich da nur 2 Threads. Drei Klassen *nebeneinander* + GUI würden vier Threads irgendwie plausibler machen.

Edit: Eventuell kann setcheckinterval() helfen, wäre aber ziemlich unsauber, denn 1. ist nicht garantiert dass es Threadwechsel betrifft, das ist jetzt nur bei CPython im Moment der Fall, und 2. betrifft es auch andere, potentiell unbekannte Sachen, mit ebenso unbekannten Auswirkungen. Threadwechsel und Signale sind ja nur Beispiele in der Dokumentation. Produktiv würde ich das wohl nicht einsetzen wollen.
Alex7713
User
Beiträge: 8
Registriert: Mittwoch 9. Oktober 2013, 14:17

Das Programm sieht in etwa so aus (der Code ist sehr lang, deswegen nur PseudoCode; und wie man richtig einrückt, keine Ahnung.-)):

# globale Variable
puffer_1
puffer_2
globale_variable_1
globale_variable_2
etc. etc.



class GUI:
____button 1
____slider
____etc. etc.
____button übernehmen

___def event_button_1():
____puffer_1 = eingabe_button

___def event_slider():
____puffer_2 = eingabe_slider

___def event_übernehmen():
____globale_variable_1 = puffer_1
____globale_variable_2 = puffer_2



class flowgraph_1:
...
class flowgraph_2
...
class flowgraph_3
...



def übernimm_GUI_werte():

___flowgraphen_eigenschaft_1 = globale_variable_1
___flowgraphen_eigenschaft_2 = globale_variable_2



__name__ == '__main__':

___thread_GUI = starte_gui_in_eigenem_thread()

___thread_GUI.start()

___thread_1 = flowgraph_1()
___thread_2 = flowgraph_2()
___thread_3 = flowgraph_3()

___while(1):

_____flowgraph_1.run()
_____flowgraph_2.run()
_____flowgraph_3.run()

_____übernimm_GUI_werte()


Die flowgraphen laufen in der While-Schleife solange, bis sie fertig sind, alle ca. 2 Sekunden. In " übernimm_GUI_werte() " werden dann die GUI-Werte, die in den globalen Variablen stehen, eingelesen und die Flowgraphs entsprechenden neu eingestellt und die while-Schleife läuft von Neuem an.

Der GUI-Thread läuft parallel, also auch während der 3*2 = 6 Sekunden reagiert die GUI. Sie schreibt in die Puffer, erst wenn der spezielle Button gedrückt wird, werden die Puffer in die globalen Variablen geschrieben und bei nächsten Aufruf von übernimm_GUI_werte() auch in den flowgraphs wirksam.

Meine Fragen:

1)
" event_übernehmen() " wird vom GUI-Thread ausgeführt. Wie schaffe ich es, dass er dabei auf keinen Fall von Sheduler unterbrochen wird, sodass z.B. nach der Häflte der Übergaben schon " übernimm_GUI_werte()" gestartet wird?

2)
" def übernimm_GUI_werte() " darf auch nicht unterbrochen werden, alle Änderungen müssen übernommen werden und DANACH die while-Scheife neu durchlaufen?

Ist es so verständlich?

Alex
BlackJack

@Alex7713: Für Quelltext gibt es Code-Tags. Schau Dir mal die Schaltflächen über dem Textfeld beim erstellen eines Beitrags an.

Also ich sehe zwei Threads. Den GUI-Thread, den ich in den Hauptthread verlegen würde, weil nicht alle GUI-Toolkits das mögen nicht dort zu laufen, und den impliziten Hauptthread in dem Deine ``while``-Schleife läuft und die Nacheinander die `run()`-Methoden der drei Flowgraphen im Hauptthread ausführt. Wenn das keine Threads sind, sollte man die vielleicht vielleicht auch nicht so nennen. Wobei das mit dem `run()` auch ein bisschen komisch aussieht weil Du das auf der *Klasse* aufrufst‽

Nach der Beschreibung suchst Du *doch* ganz einfach nur ein Lock das die zugriffe auf die globalen Variablen absichert. Threadwechsel dürfen da ruhig vorkommen, da kann dann ja nichts passieren.

Andererseits ist diese Konstruktion mit dem globalen Variablen schon irgendwie Mist. Sauberer wäre eine `Queue.Queue` zur Kommunikation zwischen den beiden Threads. Wenn in der GUI die Schaltfläche zum ”absenden” der Einstellungen betätigt wird, dann sammelt man die Werte zu einem Objekt zusammen (Wörterbuch (`dict`) oder `collections.namedtupel` zum Beispiel) und steckt die in die Queue. Und bei `uebernimm_GUI_werte()` prüft man ob Daten in der Queue stehen und übernimmt die. `Queue`-Objekte sind bereits „thread safe”, da braucht man sich keine Gedanken um Sperren machen.
Alex7713
User
Beiträge: 8
Registriert: Mittwoch 9. Oktober 2013, 14:17

"...schon irgendwie Mist."

Ouch. Aber bestreiten kann ich das unmöglich. :(

Zu dem Locking eine Frage:

Wenn ich in "def event_übernehmen()" die globalen Variablen und ihr Beschreiben in ackquire() und release() wrappe, dann wird dieser gewrappte Code definitiv am Stück ausgeführt OHNE den GUI_Thread dabei zu unterbrechen, und dieselben globalen Variablen vom Hauptthread in "übernimm_GUI_werte()" zu überbeschreiben?

Zu dem queuing eine Frage:

Ich kann also die Konstruktion mit den Puffern lassen, aber statt mit den globalen Variablen zu hantieren, schreibe ich beim Drücken des speziellen Buttons die Daten in die Warteschlage. -> Wird der put-Befehl definitiv atomar ausgeführt?

Dann rufe ich die Warteschlange wieder auf in "def übernimm_GUI_werte()"-> Der get-Befehlt ist ebenfalls atomar?

Zu dem GUI-Thread:

Ich habe im obigen Code ein Detail weggelassen. Die GUI-Klasse sieht wirklich so aus, aber es gibt noch eine Klasse, die von Threading ableitet und IN DER wird dann die GUI-Klasse instanziiert und mit mainloop() gestartet. Vermutlich ebenfalls nicht sehr gut programmiert.

Wie würde man am besten ein GUI schreiben, die den Rest des Programms nicht blockiert, sondern quasi parallel läuft und wie ich es will, nur beim Drücken des speziellen Buttons Daten aus den Puffern in die globalen Variablen/ in die queue schreibt und damit mit dem Hauptthread und den GNURadio-Sachen kommuniziert?

Alex
BlackJack

@Alex7713: Du musst schon jeden Zugriff der nicht parallel passieren darf mit einer Sperre versehen. Vorzugsweise mit der ``with``-Anweisung und dem Lock-Objekt statt `acquire()` und `release()` manuell aufzurufen.

Die Methoden zum Zugriff auf die `Queue` sind „thread safe”. `put()` und `get()` damit natürlich auch.

Wie gesagt ich würde die GUI sicherheitshalber im Hauptthread laufen lassen. Ich weiss nicht wie das bei wxWidgets ist, aber da das auf unterschiedlichen Plattformen seinerseits sogar unterschiedliche GUI-Toolkits verwendet, kann eigentlich gar nicht garantiert sein, dass das problemlos auch in anderen Threads läuft.

Falls Du übrigens eine `Thread`-Unterklasse nur schreibst, damit die `run()`-Methode eine andere Funktion ausführt, dann hast Du eine Klasse zu viel. `Thread`-Objekten kann man beim erstellen auch eine Funktion übergeben die dann mit der `start()`-Methode in einem Thread ausgeführt wird.

Die letzte Frage verstehe ich nicht ganz, denn die ist eigentlich die Antwort. Man startet den „Rechen-Thread” und dann setzt man die GUI auf und ruft im Hauptthread deren Hauptschleife auf. Wenn dann die Schaltfläche zum Übergeben der Daten gedrückt wird, sammelt man die entsprechenden Daten zusammen und steckt die in einem Objekt zusammengefasst in die Queue.

Für das Programmende muss man sich in der Regel noch etwas ausdenken. Zum Beispiel ein spezielles „Sentinel”-Objekt um dem „Rechen-Thread” das Ende zu signalisieren, damit der sich ordentlich beenden kann und nicht hart abgeschossen werden muss. `None` bietet sich da in der Regel an.
Alex7713
User
Beiträge: 8
Registriert: Mittwoch 9. Oktober 2013, 14:17

Bei der ersten Methode müsste ich also folgendes schreiben:

___def event_übernehmen():

____with lock:
______globale_variable_1 = puffer_1
____with lock:
______globale_variable_2 = puffer_2

Aber kann der Thread, in dem die GUI läuft, nicht unterbrochen werden nachdem er zwar "thread-safe" die erste Variable globale_variable_1 geändert hat, die zweite globale_variable_2 aber noch nicht. Dann wechselt der Sheduler in den Hauptthread und übernimmt nur die Änderung von globale_variable_1, aber nicht globale_variable_2.

Kann das passieren?

Oder geht auch:

___def event_übernehmen():

____with lock:
______globale_variable_1 = puffer_1
______globale_variable_2 = puffer_2

Hier werden erst beide Variablen "thread-safe" verändert und erst danach kann in den Hauptthread gewechselt werden, der kann alle Änderungen übernimmt. Mit einer Warteschlange ginge dies doch definitiv, wenn ich das nun korrekt verstanden habe.

Nochmal zu der GUI:

Meinst du das wie folgt:

__name__ == '__main__':

_____Thread = Rechen_thread()
_____Rechen_thread.start()

_____app.MainLoop()

Die GUI ist jetzt im Hauptthread und in Rechen_thread packe ich dann quasi die while(1)-schleife von oben und all die GNURadio flowgraphs.

Dann läuft die GUI im Hauptthread und der Rest im Nebenthread.

Und ich danke dir für deine Mühen. Python ist für mich in GNURadio ein wenig nur Mittel zum Zweck, im Grunde geht es mehr um den effizienten C++ Code und die Signalverarbeitung. Deswegen will ich nur sauber den Python-Code auf die Beine stellen und danach nur noch das Layout der GUI ändern und ansonsten den Python-Teil unberührt lassen.

Aber dafür muss ich die "thread-Sicherheit" erst richtig einarbeiten.
BlackJack

@Alex7713: Du musst schon *alle* Änderungen in *ein* ``with lock:`` stecken die sich zwischendurch nicht ändern dürfen. Und wie gesagt bei allen solchen Zugriffen, also sowohl wenn sie geschrieben werden als auch dort wo sie gelesen werden.

Und ja, eine `Queue.Queue` ist „thread safe”. (Irgendwie wiederhole ich mich hier. :-))
Alex7713
User
Beiträge: 8
Registriert: Mittwoch 9. Oktober 2013, 14:17

Das mit der Queue funktioniert gut, ich bin mit dem Ergebnis sehr zufrieden, es werden immer ALLE Werte gemeinsam übernommen.

Trotzdem wüsste ich gerne, warum man die GUI nicht in einem Nebenthread starten soll. Ich arbeite mit Ubuntu und es funktioniert genau wie es soll. Aber wo liegen eventuelle Nachteile?

Wie gesagt, ich habe eine Klasse für die GUI, abgeleitet von Panel, und eine Klasse abgeleitet von Threading; in der zweiten Klasse erstelle ich eine Instanz der ersten und dann ist die GUI voll funktionsfähig und der GNURadio-Teil bleibt im Hauptthread.

Alle Bedienelemente liegen ebenfalls in dem Nebenthread mit der GUI, sie hat ihre eigenen Variablen und kommuniziert mit den Hauptthread nur über die eine Queue.
BlackJack

@Alex7713: Nicht alle GUI-Toolkits unterstützen es wenn man sie nicht im Hauptthread startet. Reicht das als Grund nicht?
Alex7713
User
Beiträge: 8
Registriert: Mittwoch 9. Oktober 2013, 14:17

Als Grund ja, als Erklärung nein :D

Schau, mein Python-Programm ist simpel und funktioniert, ich würde das ungern ändern, schon allein weil mir bei jeder Änderung 1000 andere Fragen in den Sinn kommen. Aber gleichzeitig will ich das ordentlich machen. Deswegen muss ich verstehen, warum das funktioniert, obwohl es nicht unterstützt wird und was passieren kann, wenn man es so lässt.

Einem Experten mag das trivial erscheinen, aber für mich ist das wichtig.

Außerdem will ich die GNURadio-Sachen nicht aus dem Hauptthread nehmen, da mir an anderer Stelle geraten wurde, das unbedingt sein zu lassen.

Ich mache es genau wie hier im 2. Post, in der letzten while-Schleife läuft der GNURadio-Code:
http://wxpython-users.1045709.n5.nabble ... 72741.html
BlackJack

@Alex7713: Ob Dein Programm tatsächlich *grundsätzlich* funktioniert kann man nicht sagen. Sagen kannst Du nur, dass es bei Dir funktioniert. Bis jetzt. Das ist das Problem bei diesert Art von Problemen: Dass das Programm fehlerfrei läuft, heisst leider nicht, dass es fehlerfrei ist.

Der Hauptthread ist in gewisser Hinsicht etwas besonderes. Dort passieren zum Beispiel Aufräumarbeiten die als Rückruffunktionen für das Programmende registriert wurden und auch der Code der abläuft wenn das Modul am Anfang importiert läuft in diesem Thread. Das kann schon genug sein um Probleme zu bereiten wenn ansonsten von einem anderen Thread auf die Datenstrukturen vom GUI-Toolkit zugegriffen wird. Bei wxWidgets hat man zusätzlich das Problem, dass das ein „Meta-GUI-Toolkit” ist, also gar nicht die tatsächlichen GUI-Elemente bereit stellt, sondern eine gemeinsame API über verschiedene andere GUI-Toolkits legt. Also zum Beispiel Gtk unter Linux und die „native” GUI unter Windows. Das heisst hier muss man schauen was der gemeinsame kleinste Nenner zwischen allen verwendeten und vielleicht auch zukünftig verwendeten GUI-Toolkits ist, auf denen wxWidgets aufbaut.

Ich rate Dir hier genau so dringend die GUI nicht aus dem Hauptthread zu nehmen wie andere Dir den gleichen dringenden Rat für GNURadio gegeben haben. Wahrscheinlich aus den selben Gründen. :-) Was Dir jetzt wahrscheinlich nicht besonders weiterhilft. :?
Alex7713
User
Beiträge: 8
Registriert: Mittwoch 9. Oktober 2013, 14:17

Na gut, ich bin jetzt zu einer Lösung gekommen. Ich werde beides machen. Es müssen ja nur 4, 5 Zeilen geändert werden.

http://wiki.wxpython.org/MainLoopAsThread

Verstehe ich den Link falsch, oder kann man unter bestimmten Bedingungen die GUI doch in einem eigenen Thread starten? (Was du von mir grad denken musst, sturer Bock :roll: Aber die Lösung ist so simpel, ich will nicht von ihr ablassen.)

Außerdem habe ich in der Mailing-List von GNURadio meine Bedenken bzgl. GNURadio in einem seperaten Thread kundgetan und mir wurde gesagt, dass sie unbegründet sind. Das Programm wird dadurch nicht weniger performant. Es wird ohnehin für jeden Block ein eigener Thread gestartet.
BlackJack

@Alex7713: Fragst Du jetzt gerade ob Methode 3 doch geht? Also die Methode wo unten auf der Seite jemand den Absturz auf MacOS zeigt, den er damit bekommen hat, wo ziemlich deutlich drin steht, dass die GUI es nicht mag *nicht* im Hauptthread zu laufen: „[NSUndoManager(NSInternal) _endTopLevelGroupings] is only safe to invoke on the main thread.”? ;-)
Alex7713
User
Beiträge: 8
Registriert: Mittwoch 9. Oktober 2013, 14:17

MacOS, dann hat er es auch verdient :!:

Und wie gesagt, ich werde primär jetzt deinen Vorschlag verfolgen, ich weis ja nun, dass das definitiv geht und den anderen verfolge ich nur aus intellektueller Engstirnigkeit.

Aber, nochmals vielen Dank für deine Hilfe. Sollte ich mal wieder eine Frage haben, kann ich ja sicherlich auf diesen Thread zurückkommen.

Alex
Antworten