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

TypicalRun.png

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

Display

WorkingWithThreads (last edited 2011-09-10 06:21:50 by pool-71-244-98-82)

NOTE: To edit pages in this wiki you must be a member of the TrustedEditorsGroup.