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.

AltStyle によって変換されたページ (->オリジナル) /