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
#! /usr/bin/env python ############################################################################# """ Mandelbrot program, for no particular reason. John Farrell: initial version Robin Dunn: introduced wxImage.SetData instead of wxBitmapFromData Ionel Simionescu: used the Numeric package, and rewrote all of the computation and data displaying code Alexander Smishlajev: suggestions on optimising loops and drawing Markus Gritsch: in-place calculation in the mandelbrot while loop Mike Fletcher: minor changes [06/24/09] Cody Precord: Cleanup, get it working with wxPython2.8, fixes for thread safety, and fixes for old deprecated/obsolete code. """ import wx import threading # TODO update to use numpy instead of old Numeric package import numpy.oldnumeric as Numeric class MandelbrotGenerator: """Slightly slower mandelbrot generator, this one uses instance attributes instead of globals and provides for "tiling" display """ def __init__(self, width=200, height=200, coordinates=((-2.0,-1.5), (1.0,1.5)), xdivisor=0, ydivisor=0, iterations=255, redrawCallback=None, statusCallback=None): self.width = width self.height = height self.coordinates = coordinates ### should check for remainders somewhere and be a little # smarter about valid ranges for divisors (100s of divisions # in both directions means 10000s of calcs)... if not xdivisor: xdivisor = 1 #width/50 or 1 if not ydivisor: ydivisor = 10 #height/50 or 1 self.xdivisor = xdivisor self.ydivisor = ydivisor self.redrawCallback = redrawCallback self.statusCallback = statusCallback or self.printStatus self.MAX_ITERATIONS = iterations self.data = Numeric.zeros(width*height) self.data.shape = (width, height) ### Set up tiling info, should really just do all the setup here and use a ### random choice to decide which tile to compute next self.currentTile = (-1,0) self.tileDimensions = ( width/xdivisor, height/ydivisor ) self.tileSize = (width/xdivisor)* (height/ydivisor) (xa,ya), (xb,yb) = coordinates self.x_starts = Numeric.arange( xa, xb+((xb-xa)/xdivisor), (xb-xa)/xdivisor) self.y_starts = Numeric.arange( ya, yb+((yb-ya)/ydivisor), (yb-ya)/ydivisor) def DoAllTiles( self ): while self.DoTile(): pass def DoTile(self, event=None): """Triggered event to draw a single tile into the data object""" x_index, y_index = self.currentTile if x_index < self.xdivisor - 1: self.currentTile = x_index, y_index = x_index+1, y_index elif y_index < self.ydivisor-1: self.currentTile = x_index, y_index = 0, y_index+1 else: if self.redrawCallback is not None: self.redrawCallback(self.data, False) return False print 'starting iteration', x_index, y_index coords = ((self.x_starts[x_index],self.y_starts[y_index]), (self.x_starts[x_index+1],self.y_starts[y_index+1]),) part = self._tile( coords ) part.shape = self.tileDimensions[1], self.tileDimensions[0] xdiv = self.width / self.xdivisor ydiv = self.height / self.ydivisor from_idx = ydiv * y_index self.data[from_idx : ydiv * (y_index+1), xdiv * x_index: xdiv * (x_index+1), ] = part if self.redrawCallback: self.redrawCallback(self.data, True) # there may be more to do... return True def printStatus(self, *arguments ): pass #print arguments def _tile(self, coordinates): """Calculate a single tile's value""" (c, z) = self._setup(coordinates) iterations = 0 size = self.tileSize i_no = Numeric.arange(size) # non-overflow indices data = self.MAX_ITERATIONS + Numeric.zeros(size) # initialize the "todo" arrays; # they will contain just the spots where we still need to iterate c_ = Numeric.array(c).astype(Numeric.Complex32) z_ = Numeric.array(z).astype(Numeric.Complex32) progressMonitor = self.statusCallback while (iterations < self.MAX_ITERATIONS) and len(i_no): # do the calculations in-place Numeric.multiply(z_, z_, z_) Numeric.add(z_, c_, z_) overflow = Numeric.greater_equal(abs(z_), 2.0) not_overflow = Numeric.logical_not(overflow) # get the indices where overflow occured ####overflowIndices = Numeric.compress(overflow, i_no) # slower overflowIndices = Numeric.repeat(i_no, overflow) # faster # set the pixel indices there for idx in overflowIndices: data[idx] = iterations # compute the new array of non-overflow indices i_no = Numeric.repeat(i_no, not_overflow) # update the todo arrays c_ = Numeric.repeat(c_, not_overflow) z_ = Numeric.repeat(z_, not_overflow) iterations = iterations + 1 progressMonitor(iterations, 100.0 * len(i_no) / size) return data def _setup(self, coordinates): """setup for processing of a single tile""" # we use a single array for the real values corresponding to the x coordinates width, height = self.tileDimensions diff = coordinates[1][0] - coordinates[0][0] xs = 0j + (coordinates[0][0] + Numeric.arange(width).astype(Numeric.Float32) * diff / width) # we use a single array for the imaginary values corresponding to the y coordinates diff = coordinates[1][1] - coordinates[0][1] ys = 1j * (coordinates[0][1] + Numeric.arange(height).astype(Numeric.Float32) * diff / height) # we build <c> in direct correpondence with the pixels in the image c = Numeric.add.outer(ys, xs) z = Numeric.zeros((height, width)).astype(Numeric.Complex32) # use flattened representations for easier handling of array elements c = Numeric.ravel(c) z = Numeric.ravel(z) return (c, z) #### GUI #### class MandelCanvas(wx.Window): def __init__(self, parent, id=wx.ID_ANY, width=600, height=600, coordinates=((-2.0,-1.5),(1.0,1.5)), weights=(16,1,32), iterations=255, xdivisor=0, ydivisor=0): wx.Window.__init__(self, parent, id) # Attributes self.width = width self.height = height self.coordinates = coordinates self.weights = weights self.parent = parent self.border = (1, 1) self.bitmap = None self.colours = Numeric.zeros((iterations + 1, 3)) arangeMax = Numeric.arange(0, iterations + 1) self.colours[:,0] = Numeric.clip(arangeMax * weights[0], 0, iterations) self.colours[:,1] = Numeric.clip(arangeMax * weights[1], 0, iterations) self.colours[:,2] = Numeric.clip(arangeMax * weights[2], 0, iterations) self.image = wx.EmptyImage(width, height) self.bitmap = self.image.ConvertToBitmap() self.generator = MandelbrotGenerator(width=width, height=height, coordinates=coordinates, redrawCallback=self.dataUpdate, iterations=iterations, xdivisor=xdivisor, ydivisor=ydivisor) # Setup self.SetSize(wx.Size(width, height)) self.SetBackgroundColour(wx.NamedColour("black")) self.Bind(wx.EVT_PAINT, self.OnPaint) # Start generating the image self.thread = threading.Thread(target=self.generator.DoAllTiles) self.thread.start() def dataUpdate(self, data, more=False): if more: data.shape = (self.height, self.width) # build the pixel values pixels = Numeric.take(self.colours, data) # create the image data bitmap = pixels.astype(Numeric.UnsignedInt8).tostring() # create the image itself def updateGui(): """Need to do gui operations back on main thread""" self.image.SetData(bitmap) self.bitmap = self.image.ConvertToBitmap() self.Refresh() wx.CallAfter(updateGui) def OnPaint(self, event): dc = wx.PaintDC(self) dc.BeginDrawing() if self.bitmap != None and self.bitmap.IsOk(): dc.DrawBitmap(self.bitmap, 0, 0, False) dc.EndDrawing() class MyFrame(wx.Frame): def __init__(self, parent, ID, title): wx.Frame.__init__(self, parent, ID, title) self.CreateStatusBar() self.Centre(wx.BOTH) mdb = MandelCanvas(self, width=400, height=400, iterations=255) # Layout sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(mdb, 0, wx.EXPAND) self.SetAutoLayout(True) self.SetInitialSize() class MyApp(wx.App): def OnInit(self): frame = MyFrame(None, wx.ID_ANY, "Mandelbrot") frame.Show(True) self.SetTopWindow(frame) return True if __name__ == '__main__': app = MyApp(0) 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 ] ' % (msgData) ) 7 8 elif isinstance( msgData, float ) : 9 self.displayTxtCtrl.WriteText( ' ' ) # A blank line separator 10 self.displayTxtCtrl.WriteText( 'Processing time was [ %s ] secs. ' % (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 ] ' % (msgData) ) 17 18 else : 19 self.displayTxtCtrl.WriteText( 'Integer Calculation Result = [ %d ] ' % (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