Read Programming Python Online

Authors: Mark Lutz

Tags: #COMPUTERS / Programming Languages / Python

Programming Python (83 page)

BOOK: Programming Python
7.2Mb size Format: txt, pdf, ePub
ads
GUIs, Threads, and Queues

In
Chapter 5
, we
learned about threads and the queue mechanism that threads
typically use to communicate with one another. We also described the
application of those ideas to GUIs in the abstract. In
Chapter 9
, we specialized some of these
topics to the tkinter GUI toolkit we’re using in this book and expanded on
the threaded GUI model in general, including thread safety (or lack
thereof) and the roles of queues and locks.

Now that we’ve become fully functional GUI programmers, we can
finally see what these ideas translate to in terms of code. If you skipped
the related material in
Chapter 5
or
Chapter 9
, you should probably go back and
take a look first; we won’t be repeating the thread or queue background
material in its entirety here.

The application to GUIs, however, is straightforward. Recall that
long-running operations must generally be run in parallel threads, to
avoid blocking the GUI from updating itself or responding to new user
requests. Long-running operations can include time-intensive function
calls, downloads from servers, blocking input/output calls, and any task
which might insert a noticeable delay. In our packing and unpacking
examples earlier in this chapter, for instance, we noted that the calls to
run the actual file processing should generally run in threads so that the
main GUI thread is not blocked until they finish.

In the general case, if a GUI waits for anything to finish, it will
be completely unresponsive during the wait—it can’t be resized, it can’t
be minimized, and it won’t even redraw itself if it is covered and
uncovered by other windows. To avoid being blocked this way, the GUI must
run long-running tasks in parallel, usually with threads that can share
program state. That way, the main GUI thread is freed up to update the
display and respond to new user interactions while threads do other work.
As we’ve also seen, the tkinter
update
call can help in some contexts, but it only refreshes the display when it
can be called; threads fully parallelize long-running operations and offer
a more general solution.

However, because, as we learned in
Chapter 9
, only the main thread should
generally update a GUI’s display, threads you start to handle long-running
tasks should not update the display with results themselves. Rather, they
should place data on a queue (or other mechanism), to be picked up and
displayed by the main GUI thread. To make this work, the main thread
typically runs a timer-based loop that periodically checks the queue for
new results to be displayed. Spawned threads produce and queue data but
know nothing about the GUI; the main GUI thread consumes and displays
results but does not generate them.

Because of its division of labor, we usually call this a
producer
/
consumer
model—task
threads produce data which the GUI thread consumes. The long-running task
threads are also sometimes called
workers
, because
they handle the work of producing results behind the scenes, for the GUI
to present to a user. In some sense, the GUI is also a
client
to worker thread
servers
,
though that terminology is usually reserved for more specific
process-based roles; servers provide data sources which are longer-lived
and more loosely coupled (though a GUI can also display data from
independent servers). Whatever we call it, this model both avoids blocking
the GUI while tasks run and avoids potentially parallel updates to the GUI
itself.

As a more concrete example, suppose your GUI needs to display
telemetry data sent in real time from a satellite over sockets (an IPC
tool introduced in
Chapter 5
). Your program
has to be responsive enough to not lose incoming data, but it also cannot
get stuck waiting for or processing that data. To achieve both goals,
spawn threads that fetch the incoming data and throw it on a queue, to be
picked up and displayed periodically by the main GUI thread. With such a
separation of labor, the GUI isn’t blocked by the satellite, nor vice
versa—the GUI itself will run independently of the data streams, but
because the data stream threads can run at full speed, they’ll be able to
pick up incoming data as fast as it’s sent. GUI event loops are not
generally responsive enough to handle real-time inputs. Without the data
stream threads, we might lose incoming telemetry; with them, we’ll receive
data as it is sent and display it as soon as the GUI’s event loop gets
around to picking it up off the queue—plenty fast for the real human user
to see. If no data is sent, only the spawned threads wait, not the GUI
itself.

In other scenarios, threads are required just so that the GUI
remains active during long-running tasks. While downloading a reply from a
web server, for example, your GUI must be able to redraw itself if covered
or resized. Because of that, the download call cannot be a simple function
call; it must run in parallel with the rest of your
program—
typically, as a thread. When the
result is fetched, the thread must notify the GUI that data is ready to be
displayed; by placing the result on a queue, the notification is
simple—
the main GUI thread will find it the
next time it checks the queue in its timer callback function. For example,
we’ll use threads and queues this way in the PyMailGUI program in
Chapter 14
, to allow multiple overlapping mail
transfers to occur without blocking the GUI
itself.

Placing Data on Queues

Whether your GUIs interface with
satellites, websites, or something else, this thread-based
model turns out to be fairly simple in terms of code.
Example 10-18
is the GUI
equivalent of the queue-based threaded program we met earlier in
Chapter 5
(compare this with
Example 5-14
). In the context of a GUI,
the consumer thread becomes the GUI itself, and producer threads add
data to be displayed to the shared queue as it is produced. The main GUI
thread uses the tkinter
after
method
to check the queue for results instead of an explicit loop.

Example 10-18. PP4E\Gui\Tools\queuetest-gui.py

# GUI that displays data produced and queued by worker threads
import _thread, queue, time
dataQueue = queue.Queue() # infinite size
def producer(id):
for i in range(5):
time.sleep(0.1)
print('put')
dataQueue.put('[producer id=%d, count=%d]' % (id, i))
def consumer(root):
try:
print('get')
data = dataQueue.get(block=False)
except queue.Empty:
pass
else:
root.insert('end', 'consumer got => %s\n' % str(data))
root.see('end')
root.after(250, lambda: consumer(root)) # 4 times per sec
def makethreads():
for i in range(4):
_thread.start_new_thread(producer, (i,))
if __name__ == '__main__':
# main GUI thread: spawn batch of worker threads on each mouse click
from tkinter.scrolledtext import ScrolledText
root = ScrolledText()
root.pack()
root.bind('', lambda event: makethreads())
consumer(root) # start queue check loop in main thread
root.mainloop() # pop-up window, enter tk event loop

Observe how we fetch one queued data item per timer event here.
This is on purpose; although we could loop through all the data items
queued on each timer event, this might block the GUI indefinitely in
pathological cases where many items are queued quickly (imagine a fast
telemetry interface suddenly queueing hundreds or thousands of results
all at once). Processing one item at a time ensures that the GUI will
return to its event loop to update the display and process new user
inputs without becoming blocked. The downside of this approach is that
it may take awhile to work through very many items placed on the queue.
Hybrid schemes, such as dispatching at most N queued items per timer
event callback, might be useful in some such scenarios; we’ll see an
example like this later in this section (
Example 10-20
).

When this script is run, the main GUI thread displays the data it
grabs off the queue in the
ScrolledText
window captured in
Figure 10-11
. A new batch of four
producer threads is started each time you left-click in the window, and
threads issue “get” and “put” messages to the standard output stream
(which isn’t synchronized in this example—printed messages might overlap
occasionally on some platforms, including Windows). The producer threads
issue sleep calls to simulate long-running tasks such as downloading
mail, fetching a query result, or waiting for input to show up on a
socket (more on sockets later in this chapter). I left-clicked multiple
times to encourage thread overlap in
Figure 10-11
.

Figure 10-11. Display updated by main GUI thread

Recoding with classes and bound methods

Example 10-19
takes
the model one small step further and migrates it to a
class to allow for future customization and reuse. Its operation,
window, and output are the same as the prior non-object-oriented
version, but the queue is checked more often, and there are no
standard output prints. Notice how we use
bound
methods
for button callbacks and thread actions here;
because bound methods retain both instance and method, the threaded
action has access to state information, including the shared queue.
This allows us to move the queue and the window itself from the prior
version’s global variables to instance object state.

Example 10-19. PP4E\Gui\Tools\queuetest-gui-class.py

# GUI that displays data produced and queued by worker threads (class-based)
import threading, queue, time
from tkinter.scrolledtext import ScrolledText # or PP4E.Gui.Tour.scrolledtext
class ThreadGui(ScrolledText):
threadsPerClick = 4
def __init__(self, parent=None):
ScrolledText.__init__(self, parent)
self.pack()
self.dataQueue = queue.Queue() # infinite size
self.bind('', self.makethreads) # on left mouse click
self.consumer() # queue loop in main thread
def producer(self, id):
for i in range(5):
time.sleep(0.1)
self.dataQueue.put('[producer id=%d, count=%d]' % (id, i))
def consumer(self):
try:
data = self.dataQueue.get(block=False)
except queue.Empty:
pass
else:
self.insert('end', 'consumer got => %s\n' % str(data))
self.see('end')
self.after(100, self.consumer) # 10 times per sec
def makethreads(self, event):
for i in range(self.threadsPerClick):
threading.Thread(target=self.producer, args=(i,)).start()
if __name__ == '__main__':
root = ThreadGui() # in main thread: make GUI, run timer loop
root.mainloop() # pop-up window, enter tk event loop

Watch for this thread, timer loop, and shared queue technique to
resurface later in this chapter, as well as in
Chapter 11
’s more realistic PyEdit program
example. In PyEdit, we’ll use it to run external file searches in
threads, so they avoid blocking the GUI and may overlap in time. We’ll
also revisit the classic producer/consumer thread queue model in a
more realistic scenario later in this chapter, as a way to avoid
blocking a GUI that must read an input stream—the output of another
program.

Thread exits in GUIs

Example 10-19
also
uses Python’s
threading
module
instead of
_thread
. This would
normally mean that, unlike the prior version, the program would not
exit if any producer threads are still running, unless they are made
daemons manually by setting their
daemon
flag to
True
. Remember that under
threading
, programs exit when only daemonic
threads remain; the producer threads here inherit a
False
daemon value from the thread that
creates them, which prevents program exit while they run.

However, in this example the spawned threads finish too quickly
to noticeably defer program exit. Change this script’s
time.sleep
call to 2.0 seconds to simulate
longer-lived worker threads and witness this effect in action—closing
the window after a left-click erases the window, but the program
itself then does not exit for roughly 10 seconds (e.g., its shell
window is paused). If you do the same to the prior
_thread
version, or set this version’s
threads’
daemon
flags to
True
, the program exits immediately
instead.

In more realistic GUIs, you’ll want to analyze exit policies in
the context of running threads, and code accordingly; both nondaemonic
threading
threads and thread locks
in general can be used to defer exits if needed. Conversely, a
perpetually running
threading
thread might preclude a desired shutdown if nondaemonic. See
Chapter 5
for more on program exits and
daemonic threads (and other scary
topics!).

BOOK: Programming Python
7.2Mb size Format: txt, pdf, ePub
ads

Other books

The Little Hotel by Christina Stead
The Story of My Assassins by Tarun J. Tejpal
Convicted: A Mafia Romance by Macguire, Jacee
Vampire Games by J. R. Rain