Introduction
We'll need a threading recipe, this is the old Mandelbrot frame a number of us were working on way back when. Not really heavily threaded, but only thing I have that uses normal threads (not micro-threads).
See also LongRunningTasks, MainLoopAsThread.
Example
1 #! /usr/bin/env python
2 #############################################################################
3
4 """
5 Mandelbrot program, for no particular reason.
6
7 John Farrell: initial version
8 Robin Dunn: introduced wxImage.SetData instead of wxBitmapFromData
9 Ionel Simionescu: used the Numeric package, and rewrote all of the
10 computation and data displaying code
11 Alexander Smishlajev: suggestions on optimising loops and drawing
12 Markus Gritsch: in-place calculation in the mandelbrot while loop
13 Mike Fletcher: minor changes
14
15 [06/24/09] Cody Precord: Cleanup, get it working with wxPython2.8,
16 fixes for thread safety,
17 and fixes for old deprecated/obsolete code.
18
19 """
20
21 import wx
22 import threading
23 # TODO update to use numpy instead of old Numeric package
24 import numpy.oldnumeric as Numeric
25
26 class MandelbrotGenerator:
27 """Slightly slower mandelbrot generator, this one uses instance
28 attributes instead of globals and provides for "tiling" display
29
30 """
31 def __init__(self, width=200, height=200,
32 coordinates=((-2.0,-1.5), (1.0,1.5)),
33 xdivisor=0, ydivisor=0,
34 iterations=255,
35 redrawCallback=None,
36 statusCallback=None):
37
38 self.width = width
39 self.height = height
40 self.coordinates = coordinates
41
42 ### should check for remainders somewhere and be a little
43 # smarter about valid ranges for divisors (100s of divisions
44 # in both directions means 10000s of calcs)...
45 if not xdivisor:
46 xdivisor = 1 #width/50 or 1
47 if not ydivisor:
48 ydivisor = 10 #height/50 or 1
49 self.xdivisor = xdivisor
50 self.ydivisor = ydivisor
51 self.redrawCallback = redrawCallback
52 self.statusCallback = statusCallback or self.printStatus
53 self.MAX_ITERATIONS = iterations
54 self.data = Numeric.zeros(width*height)
55 self.data.shape = (width, height)
56
57 ### Set up tiling info, should really just do all the setup here and use a
58 ### random choice to decide which tile to compute next
59 self.currentTile = (-1,0)
60 self.tileDimensions = ( width/xdivisor, height/ydivisor )
61 self.tileSize = (width/xdivisor)* (height/ydivisor)
62 (xa,ya), (xb,yb) = coordinates
63 self.x_starts = Numeric.arange( xa, xb+((xb-xa)/xdivisor), (xb-xa)/xdivisor)
64 self.y_starts = Numeric.arange( ya, yb+((yb-ya)/ydivisor), (yb-ya)/ydivisor)
65
66 def DoAllTiles( self ):
67 while self.DoTile():
68 pass
69
70 def DoTile(self, event=None):
71 """Triggered event to draw a single tile into the data object"""
72 x_index, y_index = self.currentTile
73 if x_index < self.xdivisor - 1:
74 self.currentTile = x_index, y_index = x_index+1, y_index
75 elif y_index < self.ydivisor-1:
76 self.currentTile = x_index, y_index = 0, y_index+1
77 else:
78 if self.redrawCallback is not None:
79 self.redrawCallback(self.data, False)
80 return False
81
82 print 'starting iteration', x_index, y_index
83 coords = ((self.x_starts[x_index],self.y_starts[y_index]),
84 (self.x_starts[x_index+1],self.y_starts[y_index+1]),)
85 part = self._tile( coords )
86 part.shape = self.tileDimensions[1], self.tileDimensions[0]
87
88 xdiv = self.width / self.xdivisor
89 ydiv = self.height / self.ydivisor
90 from_idx = ydiv * y_index
91 self.data[from_idx : ydiv * (y_index+1), xdiv * x_index: xdiv * (x_index+1), ] = part
92 if self.redrawCallback:
93 self.redrawCallback(self.data, True) # there may be more to do...
94 return True
95
96 def printStatus(self, *arguments ):
97 pass #print arguments
98
99 def _tile(self, coordinates):
100 """Calculate a single tile's value"""
101 (c, z) = self._setup(coordinates)
102 iterations = 0
103 size = self.tileSize
104 i_no = Numeric.arange(size) # non-overflow indices
105 data = self.MAX_ITERATIONS + Numeric.zeros(size)
106 # initialize the "todo" arrays;
107 # they will contain just the spots where we still need to iterate
108 c_ = Numeric.array(c).astype(Numeric.Complex32)
109 z_ = Numeric.array(z).astype(Numeric.Complex32)
110 progressMonitor = self.statusCallback
111
112 while (iterations < self.MAX_ITERATIONS) and len(i_no):
113 # do the calculations in-place
114 Numeric.multiply(z_, z_, z_)
115 Numeric.add(z_, c_, z_)
116 overflow = Numeric.greater_equal(abs(z_), 2.0)
117 not_overflow = Numeric.logical_not(overflow)
118 # get the indices where overflow occured
119 ####overflowIndices = Numeric.compress(overflow, i_no) # slower
120 overflowIndices = Numeric.repeat(i_no, overflow) # faster
121
122 # set the pixel indices there
123 for idx in overflowIndices:
124 data[idx] = iterations
125
126 # compute the new array of non-overflow indices
127 i_no = Numeric.repeat(i_no, not_overflow)
128
129 # update the todo arrays
130 c_ = Numeric.repeat(c_, not_overflow)
131 z_ = Numeric.repeat(z_, not_overflow)
132 iterations = iterations + 1
133 progressMonitor(iterations, 100.0 * len(i_no) / size)
134 return data
135
136 def _setup(self, coordinates):
137 """setup for processing of a single tile"""
138 # we use a single array for the real values corresponding to the x coordinates
139 width, height = self.tileDimensions
140 diff = coordinates[1][0] - coordinates[0][0]
141 xs = 0j + (coordinates[0][0] + Numeric.arange(width).astype(Numeric.Float32) * diff / width)
142
143 # we use a single array for the imaginary values corresponding to the y coordinates
144 diff = coordinates[1][1] - coordinates[0][1]
145 ys = 1j * (coordinates[0][1] + Numeric.arange(height).astype(Numeric.Float32) * diff / height)
146
147 # we build <c> in direct correpondence with the pixels in the image
148 c = Numeric.add.outer(ys, xs)
149 z = Numeric.zeros((height, width)).astype(Numeric.Complex32)
150
151 # use flattened representations for easier handling of array elements
152 c = Numeric.ravel(c)
153 z = Numeric.ravel(z)
154 return (c, z)
155
156 #### GUI ####
157 class MandelCanvas(wx.Window):
158 def __init__(self, parent, id=wx.ID_ANY,
159 width=600, height=600,
160 coordinates=((-2.0,-1.5),(1.0,1.5)),
161 weights=(16,1,32), iterations=255,
162 xdivisor=0, ydivisor=0):
163 wx.Window.__init__(self, parent, id)
164
165 # Attributes
166 self.width = width
167 self.height = height
168 self.coordinates = coordinates
169 self.weights = weights
170 self.parent = parent
171 self.border = (1, 1)
172 self.bitmap = None
173 self.colours = Numeric.zeros((iterations + 1, 3))
174 arangeMax = Numeric.arange(0, iterations + 1)
175 self.colours[:,0] = Numeric.clip(arangeMax * weights[0], 0, iterations)
176 self.colours[:,1] = Numeric.clip(arangeMax * weights[1], 0, iterations)
177 self.colours[:,2] = Numeric.clip(arangeMax * weights[2], 0, iterations)
178
179 self.image = wx.EmptyImage(width, height)
180 self.bitmap = self.image.ConvertToBitmap()
181 self.generator = MandelbrotGenerator(width=width, height=height,
182 coordinates=coordinates,
183 redrawCallback=self.dataUpdate,
184 iterations=iterations,
185 xdivisor=xdivisor,
186 ydivisor=ydivisor)
187
188 # Setup
189 self.SetSize(wx.Size(width, height))
190 self.SetBackgroundColour(wx.NamedColour("black"))
191 self.Bind(wx.EVT_PAINT, self.OnPaint)
192
193 # Start generating the image
194 self.thread = threading.Thread(target=self.generator.DoAllTiles)
195 self.thread.start()
196
197 def dataUpdate(self, data, more=False):
198 if more:
199 data.shape = (self.height, self.width)
200 # build the pixel values
201 pixels = Numeric.take(self.colours, data)
202 # create the image data
203 bitmap = pixels.astype(Numeric.UnsignedInt8).tostring()
204
205 # create the image itself
206 def updateGui():
207 """Need to do gui operations back on main thread"""
208 self.image.SetData(bitmap)
209 self.bitmap = self.image.ConvertToBitmap()
210 self.Refresh()
211 wx.CallAfter(updateGui)
212
213 def OnPaint(self, event):
214 dc = wx.PaintDC(self)
215 dc.BeginDrawing()
216 if self.bitmap != None and self.bitmap.IsOk():
217 dc.DrawBitmap(self.bitmap, 0, 0, False)
218 dc.EndDrawing()
219
220 class MyFrame(wx.Frame):
221 def __init__(self, parent, ID, title):
222 wx.Frame.__init__(self, parent, ID, title)
223
224 self.CreateStatusBar()
225 self.Centre(wx.BOTH)
226 mdb = MandelCanvas(self, width=400, height=400, iterations=255)
227
228 # Layout
229 sizer = wx.BoxSizer(wx.VERTICAL)
230 sizer.Add(mdb, 0, wx.EXPAND)
231 self.SetAutoLayout(True)
232 self.SetInitialSize()
233
234 class MyApp(wx.App):
235 def OnInit(self):
236 frame = MyFrame(None, wx.ID_ANY, "Mandelbrot")
237 frame.Show(True)
238 self.SetTopWindow(frame)
239 return True
240
241 if __name__ == '__main__':
242 app = MyApp(0)
243 app.MainLoop()
Display
Here's a different approach to using a thread for long running calculations. This demo was modified to help show how, where and when the calculation thread sends its various messages back to the main GUI program. The original app is called wxPython and Threads posted by Mike Driscoll on the Mouse vs Python blog.
The "calculations" are simulated by calling time.sleep() for a random interval. Using sleep() is safe because the thread function isn't part of the wx GUI. The thread code uses function Publisher() from wx.lib.pubsub to send data messages back to the main program. When the main program receives a message from the thread it does a little bit of data decoding to determine which of four kinds of message it is. The GUI then displays the data appropriate for its decoded message type. Receiving messages is handled as events so the main program's MainLoop() can go on doing whatever else it needs to do. However, this demo is so simple that there happens to be nothing else to do in the meantime.
1 def DisplayThreadMessages( self, msg ) :
2 """ Receives data from thread and updates the display. """
3
4 msgData = msg.data
5 if isinstance( msgData, str ) :
6 self.displayTxtCtrl.WriteText( 'Textual message = [ %s ]\n' % (msgData) )
7
8 elif isinstance( msgData, float ) :
9 self.displayTxtCtrl.WriteText( '\n' ) # A blank line separator
10 self.displayTxtCtrl.WriteText( 'Processing time was [ %s ] secs.\n' % (msgData) )
11
12 elif isinstance( msgData, int ) :
13
14 if (msgData == -1) : # This flag value indicates 'Thread processing has completed'.
15 self.btn.Enable() # The GUI is now ready for another thread to start.
16 self.displayTxtCtrl.WriteText( 'Integer ThreadCompletedFlag = [ %d ]\n' % (msgData) )
17
18 else :
19 self.displayTxtCtrl.WriteText( 'Integer Calculation Result = [ %d ]\n' % (msgData) )
20 #end if
21
22 #end def
A much more flexible message encoding/decoding scheme can easily be substituted with just a little more ingenuity. See LongRunningTasks for a much more in-depth look at threading and also two-way communication. - Ray Pasco