Increasing Usefulness:Talking to program back-ends with Timers and Threads
by David McNab and Alex Tweedly.
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.
This walkthrough was originally written by David McNab (david at rebirthing dot co dot nz), and was revised by, and is currently maintained by, Alex Tweedly (alex at tweedly dot net)
Overview, Scope and Purpose
This walkthrough is targeted at PythonCard Version 0.8.2. As PythonCard grows, some of this walkthrough may go out of date, even fail - if this happens, please contact me 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.
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_initialize event handler 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_initialize(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 initialize event handler as an opportunity to set up a timer.
So change the event handler to the following:
def on_initialize(self, event): self.myTimer = timer.Timer(self.components.field1, -1) # create a timer self.myTimer.Start(5000) # launch timer, to fire every 5000ms (5 seconds)
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 timer is an unknown symbol - we need to grab it into our namespace. To do this, find the line at the top of your counter.py program which currently says
from PythonCard import model
and change it to say
from PythonCard import model, timer
Now, we have to make sure we can receive an event every time the clock 'ticks'.
You'll see in the on_initialize 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 timers which requires them to be associated with specific window widgets.
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.
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 suspect 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_initialize 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 import wx
This will give us access to Python's thread creation/dispatch functions, as well as message queues and wxPython.
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_initialize 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_initialize(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 print "myThread: starting loop" x = 10 while True: time.sleep(10) # time unit is seconds print "myThread x=%d" % x q.put(str(x)) # stick something on message queue wx.WakeUpIdle() # triggers 'idle' handlers x += 10
3. Launch the Thread
Add the following lines at the end of your on_initialize 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
4. Add an idle event handler
In the thread function above, the operative line is wx.WakeUpIdle() . 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.
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.
Copyright (c) 2003 by David McNab, david at rebirthing dot co dot nz
Copyright (c) 2005 by Alex Tweedly, alex at tweedly dot net
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.7 $ : $Author: alextweedly $ : Last updated $Date: 2006/04/06 11:00:25 $