Running MainLoop In a Separate Thread

Introduction

The MainLoop is usually the main thread of the application. However, there are rare situations where one might want to run the wx application on a separate thread. There are several methods within wxPython that check to see that the MainLoop is being run in the main thread. When it is not, an error may occur.

The Typical Method to Run Separate Threads

If you have code that must be run on a separate thread from the wx application, you will typically start a new thread to run that code from within the MainLoop of the wx application. This keeps the GUI responsive as described in LongRunningTasks. The thread may be started from an event within the wx application or it could be started before app.MainLoop() is called. Events are used to communicate with the wx application, for example, to update a windows content. Many of the tools available in the Threading package, such as Locks, are useful in building these applications.

Starting MainLoop In a Thread

Suppose you want to write a script that starts the wx application and then sends events to the GUI in order to update a window. You could wrap up the script as a function and then pass the function to the wx application that then starts the script on its own thread. However, if you really want to interact with the GUI dynamically from your script, you may want to start the GUI on a separate thread and then send events to the window from the main script. I found that doing this was not as easy as it seems.

RobinDunn describes the requirement of the main thread in an email response on the wxPython-users list: wxWindows expects that GUI operations and event dispatching can only take place in the main thread, (because of no thread safty in most GUI libs) and it takes some precautions to ensure that things work this way, and problems will probably happen eventually if the MainLoop thread is different than wxWindows "main thread." Whatever thread is the current one when wxWindows is initialized is what it will consider the "main thread." For wxPython 2.4 that happens when wxPython.wx is inported the first time. For 2.5 it will be when the wx.App object is created. End quote.

When you instantiate a wx.App() with a frame and then start the main loop on a separate thread, wx may report that the main loop is not run in the main thread. In order to resolve this, we can use the threading.Tread class to instantiate the wx.App within the definition of the run method of that class. The wx application now believes that it runs in the main thread. However, in order to access the attributes of the wx application, I use a threading.Lock to block the main thread until the wx application initializes the variables within the Thread class. Otherwise, the attributes that the main script needs to communicate with the GUI may not have been initialized and an error will be given. I found that a sleep statement can resolve this but the threading.Lock is more robust.

Example

The code below gives examples of running separate threads. The purpose of the code is to show an example of a simple image viewer. The image viewer is periodically updated with images.

Method 1 allows the main loop to be run as the main thread. The function vidSeq is a video sequencer that gets called on its own thread before the main loop is started. Essentially, the vidSeq function is passed as an argument to the application that then calls it. This method works well.

Method 2 is a quick attempt to run the main loop on a thread so that the vidSeq function can be called from the main script instead. This method actually works here but will give errors for a more complex example. I found that any use of wx.Timer causes this method to fail.

Method 3 creates an instance of threading.Thread and the wx application is initialized within the run statement. This thread automatically starts when instantiated. The start attribute has been redefined so that a threading.Lock can be used to block the main application until the critical parts of the run statement have been reached. The critical part is where self.frame is defined. This code sits in runVideoThread. When this function is called, the main loop is started and the SetData function is returned. Now vidSeq is called from the main script. It updates the images in the viewer using the SetData command.

 1 import wx
 2 import numpy
 3 import threading
 4 import time
 5 
 6 EVT_NEW_IMAGE = wx.PyEventBinder(wx.NewEventType(), 0)
 7 
 8 class ImageWindow(wx.Window):
 9  def __init__(self, parent, id=-1, style=wx.FULL_REPAINT_ON_RESIZE):
 10  wx.Window.__init__(self, parent, id, style=style)
 11 
 12  self.timer = wx.Timer
 13 
 14  self.img = wx.EmptyImage(2,2)
 15  self.bmp = self.img.ConvertToBitmap()
 16  self.clientSize = self.GetClientSize()
 17 
 18  self.Bind(wx.EVT_PAINT, self.OnPaint)
 19 
 20  #For video support
 21  #------------------------------------------------------------
 22  self.Bind(EVT_NEW_IMAGE, self.OnNewImage)
 23  self.eventLock = None
 24  self.pause = False
 25  #------------------------------------------------------------
 26 
 27  def OnPaint(self, event):
 28  size = self.GetClientSize()
 29  if (size == self.clientSize):
 30  self.PaintBuffer()
 31  else:
 32  self.InitBuffer()
 33 
 34  def PaintBuffer(self):
 35  dc = wx.PaintDC(self)
 36  self.Draw(dc)
 37 
 38  def InitBuffer(self):
 39  self.clientSize = self.GetClientSize()
 40  self.bmp = self.img.Scale(self.clientSize[0], self.clientSize[1]).ConvertToBitmap()
 41  dc = wx.ClientDC(self)
 42  self.Draw(dc)
 43 
 44  def Draw(self,dc):
 45  dc.DrawBitmap(self.bmp,0,0)
 46 
 47  def UpdateImage(self, img):
 48  self.img = img
 49  self.InitBuffer()
 50 
 51  #For video support
 52  #------------------------------------------------------------
 53  def OnNewImage(self, event):
 54  #print sys._getframe().f_code.co_name
 55 
 56  """Update the image from event.img. The eventLock should be
 57  locked by the method calling the event. If the stream is not
 58  on pause, the eventLock is released for calling method so that
 59  new image events may be called.
 60 
 61  The method depends on the use of thread.allocate_lock. The
 62  event must have the attributes, eventLock and oldImageLock
 63  which are the lock objects."""
 64 
 65  self.eventLock = event.eventLock
 66 
 67  if not self.pause:
 68  self.UpdateImage(event.img)
 69  self.ReleaseEventLock()
 70  if event.oldImageLock:
 71  if event.oldImageLock.locked():
 72  event.oldImageLock.release()
 73 
 74  def ReleaseEventLock(self):
 75  if self.eventLock:
 76  if self.eventLock.locked():
 77  self.eventLock.release()
 78 
 79  def OnPause(self):
 80  self.pause = not self.pause
 81  #print "Pause State: " + str(self.pause)
 82  if not self.pause:
 83  self.ReleaseEventLock()
 84  #------------------------------------------------------------
 85 
 86 #For video support
 87 #----------------------------------------------------------------------
 88 class ImageEvent(wx.PyCommandEvent):
 89  def __init__(self, eventType=EVT_NEW_IMAGE.evtType[0], id=0):
 90  wx.PyCommandEvent.__init__(self, eventType, id)
 91  self.img = None
 92  self.oldImageLock = None
 93  self.eventLock = None
 94 #----------------------------------------------------------------------
 95 
 96 class ImageFrame(wx.Frame):
 97  def __init__(self, parent):
 98  wx.Frame.__init__(self, parent, -1, "Image Frame",
 99  pos=(50,50),size=(640,480))
 100  self.window = ImageWindow(self)
 101  self.window.SetFocus()
 102 
 103 class ImageIn:
 104  """Interface for sending images to the wx application."""
 105  def __init__(self, parent):
 106  self.parent = parent
 107  self.eventLock = threading.Lock()
 108 
 109  def SetData(self, arr):
 110  #create a wx.Image from the array
 111  h,w = arr.shape[0], arr.shape[1]
 112 
 113  #Format numpy array data for use with wx Image in RGB
 114  b = arr.copy()
 115  b.shape = h, w, 1
 116  bRGB = numpy.concatenate((b,b,b), axis=2)
 117  data = bRGB.tostring()
 118 
 119  img = wx.ImageFromBuffer(width=w, height=h, dataBuffer=data)
 120 
 121  #Create the event
 122  event = ImageEvent()
 123  event.img = img
 124  event.eventLock = self.eventLock
 125 
 126  #Trigger the event when app releases the eventLock
 127  event.eventLock.acquire() #wait until the event lock is released
 128  self.parent.AddPendingEvent(event)
 129 
 130 class videoThread(threading.Thread):
 131  """Run the MainLoop as a thread. Access the frame with self.frame."""
 132  def __init__(self, autoStart=True):
 133  threading.Thread.__init__(self)
 134  self.setDaemon(1)
 135  self.start_orig = self.start
 136  self.start = self.start_local
 137  self.frame = None #to be defined in self.run
 138  self.lock = threading.Lock()
 139  self.lock.acquire() #lock until variables are set
 140  if autoStart:
 141  self.start() #automatically start thread on init
 142  def run(self):
 143  app = wx.PySimpleApp()
 144  frame = ImageFrame(None)
 145  frame.SetSize((800, 600))
 146  frame.Show(True)
 147 
 148  #define frame and release lock
 149  #The lock is used to make sure that SetData is defined.
 150  self.frame = frame
 151  self.lock.release()
 152 
 153  app.MainLoop()
 154 
 155  def start_local(self):
 156  self.start_orig()
 157  #After thread has started, wait until the lock is released
 158  #before returning so that functions get defined.
 159  self.lock.acquire()
 160 
 161 def runVideoThread():
 162  """MainLoop run as a thread. SetData function is returned."""
 163 
 164  vt = videoThread() #run wx MainLoop as thread
 165  frame = vt.frame #access to wx Frame
 166  myImageIn = ImageIn(frame.window) #data interface for image updates
 167  return myImageIn.SetData
 168 
 169 def runVideo(vidSeq):
 170  """The video sequence function, vidSeq, is run on a separate
 171  thread to update the GUI. The vidSeq should have one argument for
 172  SetData."""
 173 
 174  app = wx.PySimpleApp()
 175  frame = ImageFrame(None)
 176  frame.SetSize((800, 600))
 177  frame.Show(True)
 178 
 179  myImageIn = ImageIn(frame.window)
 180  t = threading.Thread(target=vidSeq, args=(myImageIn.SetData,))
 181  t.setDaemon(1)
 182  t.start()
 183 
 184  app.MainLoop()
 185 
 186 def runVideoAsThread():
 187  """THIS FUNCTION WILL FAIL IF WX CHECKS TO SEE THAT IT IS RUN ON
 188  MAIN THREAD. This runs the MainLoop in its own thread and returns
 189  a function SetData that allows write access to the databuffer."""
 190 
 191  app = wx.PySimpleApp()
 192  frame = ImageFrame(None)
 193  frame.SetSize((800, 600))
 194  frame.Show(True)
 195 
 196  myImageIn = ImageIn(frame.window)
 197 
 198  t = threading.Thread(target=app.MainLoop)
 199  t.setDaemon(1)
 200  t.start()
 201 
 202  return myImageIn.SetData
 203 
 204 def vidSeq(SetData,loop=0):
 205  """This is a simple test of the video interface. A 16x16 image is
 206  created with a sweep of white pixels across each row."""
 207 
 208  w,h = 16,16
 209  arr = numpy.zeros((h,w), dtype=numpy.uint8)
 210  i = 0
 211  m = 0
 212  while m < loop or loop==0:
 213  print i
 214  arr[i/h,i%w] = 255
 215  h,w = arr.shape
 216  SetData(arr)
 217  time.sleep(0.1)
 218  i += 1
 219  if not (i < w*h):
 220  arr = numpy.zeros((h,w), dtype=numpy.uint8)
 221  i = 0
 222  m += 1
 223 
 224 if __name__ == '__main__':
 225 
 226  if 1:
 227  #Method 1:
 228  runVideo(vidSeq)
 229 
 230  if 0:
 231  #Method 2:
 232  SetData = runVideoAsThread()
 233  vidSeq(SetData, loop=1)
 234 
 235  if 0:
 236  #Method 3:
 237  SetData = runVideoThread()
 238  vidSeq(SetData, loop=1)

Comments

Put your comments here.

When I run this with Methods 2 or 3 I get instant crash on MacOSX 10.8 (wxPython 2.9.4.0, cocoa). Has it been tested on MacOS?

/Users/steve/Desktop/foo.py:191: wxPyDeprecationWarning: Using deprecated class PySimpleApp. 
 app = wx.PySimpleApp()
2013年03月03日 05:26:02.334 Python[939:460b] *** Assertion failure in +[NSUndoManager _endTopLevelGroupings], /SourceCache/Foundation/Foundation-945.11/Misc.subproj/NSUndoManager.m:328
2013年03月03日 05:26:02.336 Python[939:460b] +[NSUndoManager(NSInternal) _endTopLevelGroupings] is only safe to invoke on the main thread.
2013年03月03日 05:26:02.482 Python[939:460b] (
 0 CoreFoundation 0x00007fff93a7e0a6 __exceptionPreprocess + 198
 1 libobjc.A.dylib 0x00007fff8e1103f0 objc_exception_throw + 43
 2 CoreFoundation 0x00007fff93a7dee8 +[NSException raise:format:arguments:] + 104
 3 Foundation 0x00007fff8e2246a2 -[NSAssertionHandler handleFailureInMethod:object:file:lineNumber:description:] + 189
 4 Foundation 0x00007fff8e28a8b7 +[NSUndoManager(NSPrivate) _endTopLevelGroupings] + 156
 5 AppKit 0x00007fff8feb432d -[NSApplication run] + 687
 6 libwx_osx_cocoau-2.9.4.0.0.dylib 0x00000001015180b3 _ZN14wxGUIEventLoop5DoRunEv + 51
 7 libwx_osx_cocoau-2.9.4.0.0.dylib 0x00000001013eb607 _ZN13wxCFEventLoop3RunEv + 55
 8 libwx_osx_cocoau-2.9.4.0.0.dylib 0x00000001012f06d8 _ZN16wxAppConsoleBase8MainLoopEv + 72
 9 _core_.so 0x0000000101002c3c _ZN7wxPyApp8MainLoopEv + 76
 10 _core_.so 0x000000010104e44f _wrap_PyApp_MainLoop + 79
 11 Python 0x00000001000c22c2 PyEval_EvalFrameEx + 26306
 12 Python 0x00000001000c4149 PyEval_EvalCodeEx + 2137
 13 Python 0x000000010003d910 function_call + 176
 14 Python 0x000000010000c362 PyObject_Call + 98
 15 Python 0x000000010001e97b instancemethod_call + 363
 16 Python 0x000000010000c362 PyObject_Call + 98
 17 Python 0x00000001000bf2c8 PyEval_EvalFrameEx + 14024
 18 Python 0x00000001000c4149 PyEval_EvalCodeEx + 2137
 19 Python 0x000000010003d910 function_call + 176
 20 Python 0x000000010000c362 PyObject_Call + 98
 21 Python 0x00000001000bcebd PyEval_EvalFrameEx + 4797
 22 Python 0x00000001000c286d PyEval_EvalFrameEx + 27757
 23 Python 0x00000001000c286d PyEval_EvalFrameEx + 27757
 24 Python 0x00000001000c4149 PyEval_EvalCodeEx + 2137
 25 Python 0x000000010003d910 function_call + 176
 26 Python 0x000000010000c362 PyObject_Call + 98
 27 Python 0x000000010001e97b instancemethod_call + 363
 28 Python 0x000000010000c362 PyObject_Call + 98
 29 Python 0x00000001000ba947 PyEval_CallObjectWithKeywords + 87
 30 Python 0x0000000100102353 t_bootstrap + 67
 31 libsystem_c.dylib 0x00007fff91b3b742 _pthread_start + 327
 32 libsystem_c.dylib 0x00007fff91b28181 thread_start + 13
)
2013年03月03日 05:26:02.483 Python[939:460b] *** Assertion failure in +[NSUndoManager _endTopLevelGroupings], /SourceCache/Foundation/Foundation-945.11/Misc.subproj/NSUndoManager.m:328

MainLoopAsThread (last edited 2013年03月03日 12:39:19 by c-67-164-160-131)

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

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