Sonntag, 17. Dezember 2017

Tkinter und Threads

Der Raspberry Pi ist ein recht populärer Minirechner, besonders für Hobbybastler und Leute, die gerne mit elektrischen Schaltung, welche vom Rechner gesteuert werden, herumspielen.
In Supportforen taucht öfters die Frage auf, wie man Messwerte von Sensoren am besten in graphischen Oberfläche (GUI) visualisiert werden können. Da Tkinter bei Python standardmäßig an Bord ist, wird dieses gerne eingesetzt (auch, wenn Qt oder GTK+ leistungsfähiger sind).

Das grundsätzliche Problem: Das Auslesen von Sensoren kann, je nach Sensor, mehr oder minder lang dauern. Liest man den Sensor über eine Funktion in der GUI aus, kann es sein, dass das Warten auf den Wert des Sensor die GUI bzw. deren Mainloop blockiert. Dies kann man recht einfach dadurch um gehen, dass das Auslesen der Sensoren in einen eigenen Thread auszulesen und die Werte über eine Queue an die GUI zu senden.

Der entsprechende Code kann z.B. wie folgt aussehen:

import tkinter as tk
from queue import Queue
from threading import Thread, Event
from time import sleep
from random import random

def generate_data(my_queue, event):
    while not event.is_set():
        data ={}
        data['value_sensor_1'] = random()
        data['value_sensor_2'] = random()
        my_queue.put(data)        
        sleep(0.7)
      

class Application(tk.Frame):
    def __init__(self, my_queue, thread_kill_event, master=None):
        super().__init__(master)
        self.my_queue = my_queue        
        self.thread_kill_event = thread_kill_event
        self.pack()
        self.create_widgets()
        self.update_labels()

    def create_widgets(self):
        self.label_sensor_1 = tk.Label(self, bg='purple', height=5, width=10, 
                                       text='Sensor 1')
        self.label_sensor_1.pack(side='left')
        self.label_sensor_2 = tk.Label(self, bg='yellow', height=5, width=10,
                                       text='Sensor 2')
        self.label_sensor_2.pack(side='right')
        self.quit = tk.Button(self, text='QUIT', fg='red',
                              command=self.exit_cleanup)
        self.quit.pack(side='bottom')

    def update_labels(self):
        self.master.after(500, self.update_labels)
        if not self.my_queue.empty():
            data = self.my_queue.get()
            if data['value_sensor_1'] < 0.5:
                self.label_sensor_1['bg'] = 'purple'
            else:
                self.label_sensor_1['bg'] = 'pink'
            if data['value_sensor_2'] < 0.5:
                self.label_sensor_2['bg'] = 'yellow'
            else:
                self.label_sensor_2['bg'] = 'blue'

    def exit_cleanup(self):
        self.thread_kill_event.set()
        self.master.destroy()

def main():
    my_queue = Queue()
    my_event = Event()
    my_thread = Thread(target=generate_data, args=(my_queue, my_event, ))
    my_thread.start()
    root = tk.Tk()
    app = Application(my_queue, my_event, master=root)
    app.master.title('TKinter Thread Demo')
    app.mainloop()

if __name__ == '__main__':
    main()

Von Prinzip her ist das eigentlich recht einfach:
Die Funktion generate_data , welche in einem eigenen Thread läuft, produziert alle 0,7 Sekunden einen zufälligen Wert zwischen 0 und 1 für zwei fiktive Sensoren, schreibt die Werte in ein Dictionary, welches dann in die Queue "geschoben" wird. Die 0,7 Sekunden Wartezeit "simulieren" dabei dir Trägheit des Sensors.
Die Klasse Application ist die eigentliche Tkinter-Anwendung. Diese besteht nur aus zwei Labeln und einem Quit-Button.
Die Methode update_labels ist die, welche die Werte aus der Queue holt und in Abhängigkeit vom Wert data['value_sensor_1'] bzw. data['value_sensor_2'] die Farbe des jeweiligen Labels ändert. Wichtig ist die Zeile

self.master.after(500, self.update_labels)

mit der festgelegt wird, dass die Methode alle 500 Millisekunden aufgerufen wird, so dass die Labels periodisch aktualisiert werden.
In der Funktion exit_cleanup wird dann noch der Thread sauber beendet, indem ein Event an die Funktion generate_data gesendet wird, worauf hin diese sich beendet, dann wird noch das Tkinter Fenster gekillt.

Wie zu sehen ist, ist das Entkoppeln von Messwerterfassungen und Messwertvisualisierung nicht weiter schwierig. Hat man mehrere verschiedene Sensoren, lässt sich das obige Beispiel auch einfach auf mehrere Threads erweitern.

Sonntag, 24. September 2017

Learning Concurrency in Python - Buchrezension

Der Name des englischsprachigen Buchs ist Programm: "Learning Concurrency in Python" hat das Ziel, eine Einführung in die nebenläufige Programmierung mit Python 3 zu geben.

Das ganze erfolgt recht umfassend auf rund 340 Seiten. Das Buch beginnt mit einem knappen Überblick über die verschiedenen Möglichkeiten, die Python bzw. zusätzliche Python-Module in Sachen nebenläufiger Programmierung bieten. Danach folgt ein allgemeiner (Python-unabhängiger) Überblick, welche Möglichkeiten und Limitierungen die Hardware bezüglich Nebenläufigkeit haben kann.
Ab dem 3. Kapitel wird es dann konkreter: es werden die Themen Threads in Python, Synchronisation, Ressourcenteilung und Kommunikation von Threads und Multiprocessing behandelt. Threads und Multiprocessing werden dabei über die jeweiligen Module threading und multiprocessing erläutert, als auch (etwas kürzer) über das Modul concurrent.futures.
Das folgenden Kapitel heißt "Event-Driven Programming" und behandelt primär das asyncio-Modul, am Ende des Kapitels gibt es aber auch (relativ kurze) Abschnitte zu den Modulen Twisted und Gevent.
Die folgenden beiden Kapitel behandeln "Reactive Programming" mittels der Module RxPY und PyFunctional sowie die Nutzung der GPU über die Module PyCUDA, Numba, Theano und PyOpenCL.
Zum Abschluss des Buchs gibt es nochmal auf rund zehn Seiten eine Zusammenfassung inklusive der Erläuterung der Vor- und Nachteile der verschiedene Wege zur nebenläufigen Programmierung mit Python.

Es werden also so ziemlich alle gängigen Bereich in Sachen Python und nebenläufiger Programmierung abgedeckt. Gut ist, dass das Buch sehr viel Bespiele enthält, welche der Autor auch alle erläutert, so dass eigentlich kaum Fragen offen bleiben. Persönlich fand ich das Kapitel zu asyncio sehr gut, da es zumindest mir (enorm) geholfen hat, dass Thema (endlich) besser zu verstehen.

Trotz der sehr guten und umfassenden Erklärungen hat das Buch aber einen ziemlich großen Mangel: in vielen Listings stimmen die Einrückungen nicht. Was so viel heißt wie, dass dort, wo syntaktisch Einrückungen sein müssen, keine sind (womit der Code in der abgedruckten Form nicht lauffähig wäre). Die Fehler sind zwar leicht zu erkennen, da immer "nur" eine Ebene fehlt. Aber für ein Fachbuch zu Python, wo Einrückungen essentiell wichtig sind, ist dieser Fehler schwer zu entschuldigen.

Nichts desto trotz ist das Buch durchaus empfehlenswert, wenn man einen umfassenden Überblick, Einblick und Einstieg ins Thema "concurrent programming" mit Python sucht. Um das Buch zu verstehen und effektiv nutzen zu können, sollte man fortgeschrittene Kenntnisse haben oder zumindest ein fortgeschrittener, ambitionierter Einsteiger sein (vorher braucht man wahrscheinlich so wie keine nebenläufigen Programme...).
Das Buch liest sich recht flüssige und sollte auch mit "normalen" Englischkenntnissen gut zu verstehen sein.
Wer sich für das Thema des Buchs interessiert, der sollte einen Blick hinein werfen.

Buchinfos:

Titel: Learning Concurrency in Python
Autor: Elliot Forbes
Verlag: Packt Publishing
ISBN: 9781787285378
Preis: 42,99 Euro (gedruckte Ausgabe + E-Book), 29,15 Euro (nur E-Book)
Erstauflage: August 2017