Welcome, guest | Sign In | My Account | Store | Cart

This recipe shows the easiest way of handling access to sockets, serial ports and other asynchronous I/O ports while running a Tkinter based GUI. It allows for a worker thread to block in a select(). Whenever something arrives it will received and inserted in a queue. The main (GUI) thread then polls the queue 10 times per second (often enough so the user will not notice any significant delay), and processes all messages that have arrived.

Python, 106 lines
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
"""
This recipe describes how to handle asynchronous I/O in an environment where
you are running Tkinter as the graphical user interface. Tkinter is safe
to use as long as all the graphics commands are handled in a single thread.
Since it is more efficient to make I/O channels to block and wait for something
to happen rather than poll at regular intervals, we want I/O to be handled
in separate threads. These can communicate in a threasafe way with the main,
GUI-oriented process through one or several queues. In this solution the GUI
still has to make a poll at a reasonable interval, to check if there is
something in the queue that needs processing. Other solutions are possible,
but they add a lot of complexity to the application.

Created by Jacob Hallén, AB Strakt, Sweden. 2001-10-17
"""
import Tkinter
import time
import threading
import random
import Queue

class GuiPart:
    def __init__(self, master, queue, endCommand):
        self.queue = queue
        # Set up the GUI
        console = Tkinter.Button(master, text='Done', command=endCommand)
        console.pack()
        # Add more GUI stuff here

    def processIncoming(self):
        """
        Handle all the messages currently in the queue (if any).
        """
        while self.queue.qsize():
            try:
                msg = self.queue.get(0)
                # Check contents of message and do what it says
                # As a test, we simply print it
                print msg
            except Queue.Empty:
                pass

class ThreadedClient:
    """
    Launch the main part of the GUI and the worker thread. periodicCall and
    endApplication could reside in the GUI part, but putting them here
    means that you have all the thread controls in a single place.
    """
    def __init__(self, master):
        """
        Start the GUI and the asynchronous threads. We are in the main
        (original) thread of the application, which will later be used by
        the GUI. We spawn a new thread for the worker.
        """
        self.master = master

        # Create the queue
        self.queue = Queue.Queue()

        # Set up the GUI part
        self.gui = GuiPart(master, self.queue, self.endApplication)

        # Set up the thread to do asynchronous I/O
        # More can be made if necessary
        self.running = 1
    	self.thread1 = threading.Thread(target=self.workerThread1)
        self.thread1.start()

        # Start the periodic call in the GUI to check if the queue contains
        # anything
        self.periodicCall()

    def periodicCall(self):
        """
        Check every 100 ms if there is something new in the queue.
        """
        self.gui.processIncoming()
        if not self.running:
            # This is the brutal stop of the system. You may want to do
            # some cleanup before actually shutting it down.
            import sys
            sys.exit(1)
        self.master.after(100, self.periodicCall)

    def workerThread1(self):
        """
        This is where we handle the asynchronous I/O. For example, it may be
        a 'select()'.
        One important thing to remember is that the thread has to yield
        control.
        """
        while self.running:
            # To simulate asynchronous I/O, we create a random number at
            # random intervals. Replace the following 2 lines with the real
            # thing.
            time.sleep(rand.random() * 0.3)
            msg = rand.random()
            self.queue.put(msg)

    def endApplication(self):
        self.running = 0

rand = random.Random()
root = Tkinter.Tk()

client = ThreadedClient(root)
root.mainloop()

This seems to be a common problem, since there is a question about how to do it a few times a month in comp.lang.pyhton. There are other solutions, involving synchronisation between threads that will allow you to handle the problem without the polling (the root.after()), but this is rather un-neat, since you tend to get raising and waiting for semaphores all over your code. In any case, a GUI already has a bunch of polling mechanisms built into it (the main event loop), so adding one won't make much difference - especially since it runs fairly seldom. The code has only been tested under Linux, but it should work on any platform with working threads.

7 comments

Laura Creighton 22 years, 5 months ago  # | flag

FYI: This isn't Tkinter specific. If you want to replace the Tkinter code with wxPython code, that will work as well. Laura Creighton lac@strakt.com

Boudewijn Rempt 22 years ago  # | flag

Here's the same, but for PyQt.

"""

This recipe describes how to handle asynchronous I/O in an environment
where you are running PyQt as the graphical user interface. PyQt is
safe to use as long as all the graphics commands are handled in a
single thread. Since it is more efficient to make I/O channels to
block and wait for something to happen rather than poll at regular
intervals, we want I/O to be handled in separate threads. These can
communicate in a threasafe way with the main, GUI-oriented process
through one or several queues. In this solution the GUI still has to
make a poll at a reasonable interval, to check if there is something
in the queue that needs processing. Other solutions are possible, but
they add a lot of complexity to the application.

Created by Jacob Hallén, AB Strakt, Sweden. 2001-10-17
Adapted by Boudewijn Rempt, Netherlands. 2002-04-15
"""

import sys, time, threading, random, Queue, qt

class GuiPart(qt.QMainWindow):

    def __init__(self, queue, endcommand, *args):
        qt.QMainWindow.__init__(self, *args)
        self.queue = queue
        # We show the result of the thread in the gui, instead of the console
        self.editor = qt.QMultiLineEdit(self)
        self.setCentralWidget(self.editor)
        self.endcommand = endcommand

    def closeEvent(self, ev):
        """
        We just call the endcommand when the window is closed
        instead of presenting a button for that purpose.
        """
        self.endcommand()

    def processIncoming(self):
        """
        Handle all the messages currently in the queue (if any).
        """
        while self.queue.qsize():
            try:
                msg = self.queue.get(0)
                # Check contents of message and do what it says
                # As a test, we simply print it
                self.editor.insertLine(str(msg))
            except Queue.Empty:
                pass


class ThreadedClient:
    """
    Launch the main part of the GUI and the worker thread. periodicCall and
    endApplication could reside in the GUI part, but putting them here
    means that you have all the thread controls in a single place.
    """
    def __init__(self):
        # Create the queue
        self.queue = Queue.Queue()

        # Set up the GUI part
        self.gui=GuiPart(self.queue, self.endApplication)
        self.gui.show()

        # A timer to periodically call periodicCall :-)
        self.timer = qt.QTimer()
        qt.QObject.connect(self.timer,
                           qt.SIGNAL("timeout()"),
                           self.periodicCall)

(comment continued...)

Boudewijn Rempt 22 years ago  # | flag

(...continued from previous comment)

        # Start the timer -- this replaces the initial call to periodicCall
        self.timer.start(100)

        # Set up the thread to do asynchronous I/O
        # More can be made if necessary
        self.running = 1
        self.thread1 = threading.Thread(target=self.workerThread1)
        self.thread1.start()


    def periodicCall(self):
        """
        Check every 100 ms if there is something new in the queue.
        """
        self.gui.processIncoming()
        if not self.running:
            root.quit()

    def endApplication(self):
        self.running = 0


    def workerThread1(self):
        """
        This is where we handle the asynchronous I/O. For example, it may be
        a 'select()'.
        One important thing to remember is that the thread has to yield
        control.
        """
        while self.running:
            # To simulate asynchronous I/O, we create a random number at
            # random intervals. Replace the following 2 lines with the real
            # thing.
            time.sleep(rand.random() * 0.3)
            msg = rand.random()
            self.queue.put(msg)


rand = random.Random()
root = qt.QApplication(sys.argv)

client = ThreadedClient()
root.exec_loop()

It's amazing how similar these are. Two improvements: it doesn't use sys.exit, but asks the QApplication instance to quit, and it shows the result of the thread in the gui.

Alexander Belchenko 18 years, 9 months ago  # | flag

Set thread subprocess as daemon. There is must be set thread as daemon by self.thread1.setDaemon(1) method:

self.running = 1
self.thread1 = threading.Thread(target=self.workerThread1)
self.thread1.setDaemon(1)
self.thread1.start()
Dirk Swart 12 years, 11 months ago  # | flag

Here is an updated version of Boudewiyn's code, for PyQT, Qt 4.7

"""
http://code.activestate.com/recipes/82965-threads-tkinter-and-asynchronous-io/

This recipe describes how to handle asynchronous I/O in an environment
where you are running PyQt as the graphical user interface. PyQt is
safe to use as long as all the graphics commands are handled in a
single thread. Since it is more efficient to make I/O channels to
block and wait for something to happen rather than poll at regular
intervals, we want I/O to be handled in separate threads. These can
communicate in a threasafe way with the main, GUI-oriented process
through one or several queues. In this solution the GUI still has to
make a poll at a reasonable interval, to check if there is something
in the queue that needs processing. Other solutions are possible, but
they add a lot of complexity to the application.

Created by Jacob Halln, AB Strakt, Sweden. 2001-10-17
Adapted by Boudewijn Rempt, Netherlands. 2002-04-15
Updated to Qt 4.7 by Dirk Swart, Ithaca, NY. 2011-04-15
"""

import sys, time, threading, random, Queue
from PyQt4 import QtGui, QtCore as qt

class GuiPart(QtGui.QMainWindow):

    def __init__(self, queue, endcommand, *args):
        QtGui.QMainWindow.__init__(self, *args)
        self.queue = queue
        # We show the result of the thread in the gui, instead of the console
        self.editor = QtGui.QTextEdit(self)
        self.setCentralWidget(self.editor)
        self.endcommand = endcommand



    def closeEvent(self, ev):
        """
        We just call the endcommand when the window is closed
        instead of presenting a button for that purpose.
        """
        self.endcommand()

    def processIncoming(self):
        """
        Handle all the messages currently in the queue (if any).
        """
        while self.queue.qsize():
            try:
                msg = self.queue.get(0)
                # Check contents of message and do what it says
                # As a test, we simply print it
                self.editor.insertPlainText(str(msg))
            except Queue.Empty:
                pass
Dirk Swart 12 years, 11 months ago  # | flag
class ThreadedClient:
    """
    Launch the main part of the GUI and the worker thread. periodicCall and
    endApplication could reside in the GUI part, but putting them here
    means that you have all the thread controls in a single place.
    """
    def __init__(self):
        # Create the queue
        self.queue = Queue.Queue()

        # Set up the GUI part
        self.gui=GuiPart(self.queue, self.endApplication)
        self.gui.show()

        # A timer to periodically call periodicCall :-)
        self.timer = qt.QTimer()
        qt.QObject.connect(self.timer,
                           qt.SIGNAL("timeout()"),
                           self.periodicCall)
        # Start the timer -- this replaces the initial call to periodicCall
        self.timer.start(100)

        # Set up the thread to do asynchronous I/O
        # More can be made if necessary
        self.running = 1
        self.thread1 = threading.Thread(target=self.workerThread1)
        self.thread1.start()


    def periodicCall(self):
        """
        Check every 100 ms if there is something new in the queue.
        """
        self.gui.processIncoming()
        if not self.running:
            root.quit()

    def endApplication(self):
        self.running = 0


    def workerThread1(self):
        """
        This is where we handle the asynchronous I/O. For example, it may be
        a 'select()'.
        One important thing to remember is that the thread has to yield
        control.
        """
        while self.running:
            # To simulate asynchronous I/O, we create a random number at
            # random intervals. Replace the following 2 lines with the real
            # thing.
            time.sleep(rand.random() * 0.3)
            msg = rand.random()
            self.queue.put(msg)

rand = random.Random()
root = QtGui.QApplication(sys.argv)
client = ThreadedClient()
sys.exit(root.exec_())
Nathan Phillip Brink 7 years, 4 months ago  # | flag

@Laura Creighton, well, I assume that wxPython doesn’t have .after(). The only real thing you need to know is that .after() is a thing to implement this.

It would be much nicer of course if there were a simple .post() API like C# winform’s .Invoke() added to the class library. Then all this complication wouldn’t be needed in all apps that want to use background threads.