This article is intended to give a simple overview (initially anyway) about how to write a program with a non-blocking GUI in wxPython. This topic seems to present difficulty to people on a regular basis as it is often brought up on mailing list. So hopefully through the information in this article one can see that there is nothing magic, complex, or difficult about working with threads in python/wxPython.
The scope of this article is only intended to be basic and practical. There are many topics related to threading that are not discussed in detail in this document and are only mentioned as side topics that will be left up to you the reader to explore if you so choose.
Though this article will try to keep it simple and show through example. Some background and understanding of the following topics would serve you well not only in this article but in your computing life in general:
- Operating Systems
- Processor Scheduling
Also ask yourself the following two questions and if feel you can answer them, then you should be more than ready to go:
- What is a process?
- What is the difference between a process and a thread?
2. Basic application
An application typically runs as a process with a single thread of execution. This means that as the process runs its tasks are presented to the processor (CPU) to be executed one at a time. For example, let's look at the following code (which we will build upon throughout this article).
1 import wx 2 import time 3 4 ID_COUNT = wx.NewId() 5 6 #-----------------------------------------------------------------------------# 7 8 class CountingFrame(wx.Frame): 9 def __init__(self, parent): 10 wx.Frame.__init__(self, parent, title="Lets Count", size=(300, 300)) 11 12 # Attributes 13 14 # Layout 15 self.__DoLayout() 16 self.CreateStatusBar() 17 18 # Event Handlers 19 20 def __DoLayout(self): 21 sizer = wx.BoxSizer(wx.HORIZONTAL) 22 sizer.Add(CountingPanel(self), 1, wx.ALIGN_CENTER) 23 self.SetSizer(sizer) 24 self.SetMinSize((300, 300)) 25 26 #-----------------------------------------------------------------------------# 27 28 class CountingPanel(wx.Panel): 29 def __init__(self, parent): 30 wx.Panel.__init__(self, parent) 31 32 # Attributes 33 self._counter = wx.StaticText(self, label="0") 34 self._counter.SetFont(wx.Font(16, wx.MODERN, wx.NORMAL, wx.NORMAL)) 35 36 # Layout 37 self.__DoLayout() 38 39 # Event Handlers 40 self.Bind(wx.EVT_BUTTON, self.OnButton) 41 42 def __DoLayout(self): 43 sizer = wx.BoxSizer(wx.VERTICAL) 44 button = wx.Button(self, ID_COUNT, "Increment Counter") 45 sizer.AddMany([(button, 0, wx.ALIGN_CENTER), 46 ((15, 15), 0), 47 (self._counter, 0, wx.ALIGN_CENTER)]) 48 self.SetSizer(sizer) 49 50 def OnButton(self, evt): 51 # This thread is fairly lazy by itself so it takes it 52 # 10 seconds to figure out what the next value is. 53 time.sleep(10) 54 val = int(self._counter.GetLabel()) + 1 55 self._counter.SetLabel(unicode(val)) 56 57 #-----------------------------------------------------------------------------# 58 59 if __name__ == '__main__': 60 APP = wx.App(False) 61 FRAME = CountingFrame(None) 62 FRAME.Show() 63 APP.MainLoop()
When the above script is run it will make a simple frame with a button on it. The goal of this application is to increment the count each time you click the button. This application is a process with a single thread of execution. So the Use Case / Flow is as follows.
- Click Button
OnButton is called
- Sleep for 10 seconds to simulate long calculation time
- Counter incremented
Each step in this flow can only be done one item at a time, Step 2 can't begin until Step 1 has finished, Step 3 can't begin until Step 2 is complete, etc.
So when we reach Step 3 in the flow of this application the code running in the OnButton handler will be occupying the processor and blocking the other code that is running behind the scenes for redrawing the screen, doing updates, etc. This can easily be seen by the fact that the button still appears to be pressed, a busy cursor, and the fact that you can't even move the frame to another part of the desktop until the OnButton handler has returned.
Typically for most applications this flow of control works all right and has the benefits of being easy to understand and easy to debug. However for applications that perform actions that can take a long time to complete like our calculations required for incrementing the counter. Will require support for executing code in an asynchronous manner, by adding additional threads.
3. Thread overview
This section is meant to give a simple view and understanding of some threading concepts so that some of the issues that one needs to be aware of when using threads can be understood.
When working with threads one needs to be careful to maintain thread safety. The ways to maintain this are many and their details are outside the scope of the discussion in this article. For the sake of simplicity think of thread safety by applying the following situation to computing.
There are two roads (threads) with cars (tasks) running on them, these two roads are separate but at certain points they cross where they need to share the same space (resource).
Ok from this simple example we can see a basic issue of thread safety. When the roads cross some care needs to be take to make sure that the cars coming from each road don't collide with each other at that point by employing some scheduling to regulate when each road is allowed to have a car in the shared section of the road. For further reading on ways to control traffic take a look at Locks/Mutex (stop signs), and Semaphores (metered traffic).
Luckily for most of our needs wx already has some ways to work with this through the event mechanism. Python also has support for these concepts built right in to the threading module. In the next section we will expand the counting program to support multiple worker threads to help with the counting so the main gui thread can be free to keep the UI from freezing.
4. Communicating with the main thread
Most GUIs run on a single thread and the resources attached to these GUI objects can only safety be accessed by that main thread that the GUI is running on and wx is no exception here. In our example above everything is running in the main thread. Since in our user code we don't have a way to guarantee the thread safety of accessing the gui objects on the main thread we need to use a mechanism of some sort to safety return our worker data back to the main thread without directly calling the GUI objects from the worker thread(s). Again there are a number of ways to achieve this each being suitable to different applications.
One way to achieve this is through the use of Events and since events should be a fairly familiar topic to most we will use them to solve our frozen GUI problem in the counting program.
5. Communicating with events
First step is to create a new event type to use for our communication line. So examine the following snippet and add it to our original example.
1 myEVT_COUNT = wx.NewEventType() 2 EVT_COUNT = wx.PyEventBinder(myEVT_COUNT, 1) 3 class CountEvent(wx.PyCommandEvent): 4 """Event to signal that a count value is ready""" 5 def __init__(self, etype, eid, value=None): 6 """Creates the event object""" 7 wx.PyCommandEvent.__init__(self, etype, eid) 8 self._value = value 9 10 def GetValue(self): 11 """Returns the value from the event. 12 @return: the value of this event 13 14 """ 15 return self._value
This will be used to transport our worker threads return values back to the main thread when they are ready.
Next we need to move our "heavy" counting code off into a worker thread so that it doesn't block our GUI, and so we can count faster by getting more workers to help us count while we wait for the results from the first one. This is easily done. Threads in Python can be used in an easy to understand object oriented manner, the below code can be used as a template for most/all your threading needs in Python.
As above, simply add this code to our example program.
1 import threading 2 3 class CountingThread(threading.Thread): 4 def __init__(self, parent, value): 5 """ 6 @param parent: The gui object that should recieve the value 7 @param value: value to 'calculate' to 8 """ 9 threading.Thread.__init__(self) 10 self._parent = parent 11 self._value = value 12 13 def run(self): 14 """Overrides Thread.run. Don't call this directly its called internally 15 when you call Thread.start(). 16 """ 17 time.sleep(10) # our simulated calculation time 18 evt = CountEvent(myEVT_COUNT, -1, self._value) 19 wx.PostEvent(self._parent, evt)
Now finally since we have moved our counting code off onto a worker thread we need to change the OnButton event handler to take advantage of this as well as move the code that updates the GUI to the handler for our new count events that will be coming in from our worker threads.
Also add a call to
1 self.Bind(EVT_COUNT, self.OnCount)
in the __init__ method for our CountingPanel.
That's it, we now have a multi-threaded program. No magic and nothing difficult. Run it once and try it out to see the difference in how the program can be used with this simple change. We can now click the button repetitively since the OnButton method is no longer blocking with the counting code. This also greatly improves our counting performance since before we could at most increment our count by 1 every 10 seconds but now we can count way past 1 in that same period while each call is still doing the same amount of work as before.
6. Notes about this example
In this particular example the threads will be finishing and returning in a sequential manner since each thread takes the same amount of work to finish. Because of this we don't have to worry too much about scheduling and handling of results that come back out of order.
Using events to pass back data will work well in most cases but if you have a or many worker threads that send results at a very rapid rate and in high volume there are better solutions to improve performance and reduce overhead. As handling the high volume of events can once again lead to blocking on the main thread. These other approaches are also not very difficult, I may add a supplemental example at some time if there is interest.
- Add links to external documents
- Add more examples