Increasing
Usefulness:
Talking to program back-ends with Timers and Threads
by David McNab
This
walkthrough supplements the excellent existing PythonCard
'Getting Started' walkthroughs by Dan
Shafer and David
Primmer, and follows on from the 'How to Add a child window' lesson. It's based on techniques taken from the
various PythonCard sample programs.
Overview, Scope
and Purpose
This walkthrough is targeted at PythonCard
Version 0.7. As PythonCard grows, some of this walkthrough may go
out of date, even fail - if this happens, please contact david at rebirthing dot co dot nz,
and I'll update it.
The purpose of
this walkthrough is to empower you to make your PythonCard programs
capable of much more meaningful work, by acquainting you with two
mechanisms - timers and threads, which can be used for communication
between your programs' graphical front-ends and back-ends.
Most of the
top-level code you add to the front ends - your PythonCard user
interfaces - is event handlers.
As with programming with any GUI, event handlers should always complete
their job fast, and return promptly, so they don't 'clog up the works'.
PythonCard is no exception.
But there will be many cases where you need some real-time
functionality - back-end code which runs autonomously of user interface
events.
An example of
this is programs which communicate in real-time on the Internet, or need
to interact in real time with other software on your system (eg
monitoring system load).
Timers and
Threads are two good mechanisms that allow you to separate your program
into:
- Front-End - logic which processes user interaction events
- Back-End - logic which manages non-user-interface aspects of your
program, talks to other programs on your system or across the internet,
and possibly communicates relevant updates to the user interface.
By using timers
and/or threads, you can guarantee that your PythonCard event handlers
will terminate promptly, and that your user interface can communicate as
needed with your back-end logic.
Timers
You can set up
your window class so that an event handler gets triggered at regular
intervals - the 'tick of the clock'.
This is useful for things like a time display on your window, or
polling for some external event (for instance, incoming mail), and
dozens of other situations.
Let us now add a
timer to the example code you've been writing. This timer will
automatically add 10 to the number in the counter field, doing this
every 5 seconds.
Firstly, you
will need to add an on_openBackground event to your main
window. You may have already done this, while experimenting in your
learning process during the earlier walkthroughs. But if you haven't yet
done so, add the following method code to your Counter class:
def
on_openBackground(self, event):
print "Window opened"
Not very
significant - but do save your file, and run counter.py. You'll see a
message on stdout when the window opens.
So far, so good.
Nice to know that we can receive an event when the window gets opened.
But not very useful yet.
Now, we need to
use this openBackground event as an
opportunity to set up a timer.
So change the event handler to the following:
def
on_openBackground(self, event):
self.myTimer = wx.wxTimer(self.components.field1,
-1) # create a timer
self.myTimer.Start(5000) # launch timer, to fire
every 5000ms (5 seconds)
Here, we're
making recourse to the wxPython classes underlying PythonCard. We need to do this
because PythonCard doesn't yet have its own timer wrappers. You'll also
notice from the self.components.field1
that the timer is being created in respect of the field1 widget. More on this later.
Don't try to run this program yet - it will barf since wx is an unknown symbol
- we need to grab it into our namespace. To do this, add to the top of
your counter.py program the following:
from
wxPython import wx
so that wx is a known symbol.
Now, we have to
make sure we can receive an event every time the clock 'ticks'.
You'll
see in the on_openBackground
event handler above that we've linked the timer to field1.
While conceptually the timer applies to the window as a whole, there's
a weird quirk in wx which
requires timers to be associated with specific window widgets. So we'll
just appease wx and get on with
the job.
To receive the clock
tick events, we only have to add another handler.
As per the event handler naming convention, (where widgets' handlers
are called on_componentName_event, we'll call this
handler on_field1_timer, since timer events
get directed to the widget field1, and the event is
called timer.
Now, add the
following method code into class Counter:
def
on_field1_timer(self, event):
print "Got a timer event"
startValue = int(self.components.field1.text)
endValue = startValue + 10
print "Got a timer event, value is now %d" % endValue
self.components.field1.text = str(endValue)
# uncomment the line below if you've already
followed the 'child window' walkthrough
#self.minimalWindow.components.field1.text =
str(endValue)
Note - this is ugly,
because there's a lot of duplicated functionality. We'll leave it to you
to factorise your code appropriately, creating a generic 'increment'
method which accepts an optional amount argument (default
1). But if you're impatient, don't worry about any factorisation, just
use the above code and all will be ok.
Now, save your counter.py program and run it.
You should see the number increasing by 10, every 5 seconds. I don't
think I need to say any more here - you've got the basic structure - the
rest is now up to your imagination.
You could use
timer events to poll for external conditions, but this can get real ugly
real fast. So in the next section, we'll explore a nicer and more
general way to tie your front end code to back end functionality, using threads.
Threads
Python is
beautiful in its support of threads - the ability to split up code into
multiple threads
of control,
just like an operating system does when it gives lots of separate
programs a share of the CPU.
You can ignore
the objections of "Python
prudes"
who insist that threads are not good programming practice. Sure, threads
have their pitfalls, such as deadlocks, race conditions etc, but if you
use a bit of common sense, and design intelligently, you can avoid these
pitfalls. Also, programming to get around the need for threads can
pervert your program logic, kind of like pushing your head down through
your body and out your back orifice. Not always a pretty sight :p
Note
- one actual risk of threading in Python is a syndrome called Global
Interpreter Lock,
or GIL
for short. GIL can strike in strange places, and cause one or more
threads in your program to freeze up for no apparent reason. If you ever
have reason to syspect GIL
is occurring, simply sprinkle a few print
statements in each of your threads until you either calm your
suspicions, or nail the culprit. For example, I suffered a GIL
once because a thread was building regular expression objects with re.compile().
I fixed this by building the re
objects in advance, in the main thread. I suspect this is a Python bug
(I'm using 2.2), but that's another topic.
What we'll be
doing here is adding a background thread to your counter program, which
(surprise, surprise) writes values (in this case, counting from 0 in
steps of 20) to your counter value.
The first thing
you could do is disable the timer you set up in the previous section, by
commenting out the self.myTimer.Start(5000) statement in your on_openBackground handler (see above).
This will avoid confusion for now, since there won't be a running timer
to complicate things.
Now, add to the
top of counter.py the following
statement:
import
thread, Queue
This will give
us access to Python's thread creation/dispatch functions, as well as message queues.
A little theory
I'll keep this
short and sweet. Simply, the safest way for threads to communicate with
each other is via some kind of synchronised objects. We'll use Python's
standard message queues (standard Python module Queue), since it's easy and safe and
well supported within Python. When your thread wants to send an event to
your user interface code, it will send a message to it, then 'wake up'
your user interface so that it receives an idle event. The idle event will check the
message queue, and react accordingly.
Note - when your
window classes have an idle
event handler, this handler can get triggered by all sorts of things,
particularly when your user interface falls 'idle' - mouse stops moving,
button click is finished etc. Within the idle event handler, we need to
check our message queue so we know when we need to react to
something in the back end.
Hint - run any
PythonCard program with a '-m'
argument. You'll see the program come up with a 'Message Watcher'
window. Unclick the Ignore Unused
checkbox. Interact with your program with the mouse, and you'll see a
flood of events being generated. This is a great way of "cheating" to
find out what name you'll need to give your event handlers. Another
'cheat' is to run the 'widgets' sample program, which allows you to
generate HTML documentation for the various PythonCard widgets.
Threads
Walkthrough - Summary of Steps Involved:
- Add a
message queue to our Counter class.
- Add your
thread code to counter.py, but
as a global function, not a class method. This thread will
periodically sends messages to our window
- In your on_openBackground handler, launch your
thread and pass it a handle to the message queue.
- Add an idle event handler, which
picks up these messages
- Within the idle event
handler method, check the message queue and react accordingly
1. Add the message
queue
Refer back to
the on_openBackground(self,
event)
handler above, and add the following statement:
# Add a message queue
self.msgQueue = Queue.Queue()
This sticks a
message queue into our Counter window class, that
will be used for communication from the thread backend to the foreground
window class.
2. Add a Thread
Function
Add the
following global function to your counter.py:
def
myThread(*argtuple):
"""
A little thread we've added
"""
print "myThread: entered"
q = argtuple[0]
print "myThread: starting loop"
x = 10
while 1:
time.sleep(10) # time unit is
seconds
print "myThread x=%d" % x
q.put(str(x)) # stick something
on message queue
wx.wxWakeUpIdle() # triggers
'idle' handlers
x += 10
3. Launch the Thread
Add the
following lines at the end of your on_openBackground handler:
#
Now launch the thread
thread.start_new_thread(myThread,
(self.msgQueue,))
Notice
that we have to pass the queue object to the thread in a tuple - refer
to the Python Library Reference doco for module thread
4. Add an idle
event handler
In the thread
function above, the operative line is wx.wxWakeUpIdle(). Upon calling that
method, wxWindows tells all open
windows, Hey,
wake up - something's happened you might need to react to! So we need to add a
handler to our Counter class to handle idle
events, and thus get triggered when we get 'woken up'. So add the
following method into your Counter class:
def
on_idle(self, event):
print "on_idle entered"
5. Check the
message queue and react accordingly
Presently, our
'idle' handler isn't very useful - but if you run counter.py, you'll see
it gets triggered every time the program falls idle. So now, we'll make
it do what it needs to do - reacting to events from our background
thread. Replace the idle handler above with the following:
def on_idle(self, event):
print "on_idle
entered"
while not
self.msgQueue.empty():
# Handle messages from our thread
msg = self.msgQueue.get()
print "on_idle: msg='%s'" % msg
self.components.field1.text = msg
# uncomment the following if you've followed the 'child window'
walkthrough
#self.minimalWindow.components.field1.text = msg
Now, we're all
done. Launch your counter.py, and watch as the background thread
launches, and periodically sends its messages to the user interface,
which displays these on the window.
Conclusion
During this
walkthrough, you have explored timers and threads, two easy and powerful
ways to interface your user interface classes with the back-end of a
program (where the 'real work'
happens).
Having got a handle on front-end/back-end interactions, via threads and
timers, you are now empowered to add some serious functionality to your
PythonCard programs.
There is now nothing stopping you from writing any kind
of application in PythonCard.
A common situation in programming is where you want a program to be
always running (as a Unix daemon or a Windows NT/2k/XP 'service'), but
you don't always want its window showing. A typical approach is:
- Create the back-end code as a standalone 'daemon' program (or
Windows NT/2k/XP 'service'), which talks via socket connection to the
front end.
- Create the front-end code, which operates the user interface
- Put a thread into your front end code which does the socket
communication to the daemon, and relays commands/responses/status info
between the daemon and the user interface.
With this approach, you can launch and
terminate the user interface program as you like, without disrupting the
backend in any way.
So, it's over to you now. Play around with your walkthrough programs
and the PythonCard sample programs, raid the Vaults of
Parnassus for useful bits of code, and hack to your heart's content.
The only limit is your imagination and (rapidly growing) level of
Python skill.
Happy programming!
This walkthrough was written with Mozilla
Composer (debian).
Copyright (c) 2003 by David McNab, david at rebirthing dot co dot nz.
Please feel free to mirror, copy, translate, upgrade and restribute
this page,
as long as you keep up to date with Python and PythonCard,
and credit the original author.
$Revision: 1.1 $, documentation updated
on $Date: 2003/02/18 16:33:43 $