Thursday, November 17, 2011

Multithreading in wxPython

This innocent code below looks okay, but it has one major bug here. The bug can cause the application to crash. This is because we try to perform some GUI related stuff in another thread (MyThread). Many GUI toolkits are single-threaded. In other words, the main loop (the loop that starts the GUI application) should only be responsible for handling any GUI related stuff. Any other long running tasks can be done in another thread.
#!/usr/bin/env python

import wx, threading

class MyFrame(wx.Frame):
    def __init__(self):
        wx.Frame.__init__(self, None, -1, "MyApp")
        panel = wx.Panel(self)
        
        self.btn = wx.Button(panel, label="Click Me!")
        self.btn.Bind(wx.EVT_BUTTON, self._do_something)
        
    def _do_something(self, evt):
        MyThread(self).start()
        
class MyThread(threading.Thread):
    def __init__(self, frame):
        threading.Thread.__init__(self)
        self.frame = frame
        
    def run(self):
        wx.MessageDialog(self.frame, message="Test", caption="Test",
                         style=wx.ICON_ERROR | wx.CENTRE).ShowModal()
                             
if __name__ == "__main__":
    app = wx.PySimpleApp()
    frame = MyFrame()
    frame.Centre()
    frame.Show(True)
    app.MainLoop()
To fix this bug, wxPython provides some thread-safe methods, such as wx.CallAfter, wx.CallLater, and wx.PostEvent. A combination of Publisher/Subscribe and wx.CallAfter can eliminate the bug as shown below.
#!/usr/bin/env python

import wx, threading
from wx.lib.pubsub import Publisher

class MyFrame(wx.Frame):
    def __init__(self):
        wx.Frame.__init__(self, None, -1, "MyApp")
        panel = wx.Panel(self)
        
        self.btn = wx.Button(panel, label="Click Me!")
        self.btn.Bind(wx.EVT_BUTTON, self._do_something)
        
        Publisher().subscribe(self._update, "update")
        
    def _do_something(self, evt):
        MyThread().start()
    
    def _update(self, msg):
        # msg.data is the data that was sent in the CallAfter
        wx.MessageDialog(self, message=msg.data, caption="Test",
                         style=wx.ICON_ERROR | wx.CENTRE).ShowModal()
        
class MyThread(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)
        
    def run(self):
        msg = "Test Message"
        wx.CallAfter(Publisher().sendMessage, "update", msg)
                             
if __name__ == "__main__":
    app = wx.PySimpleApp()
    frame = MyFrame()
    frame.Centre()
    frame.Show(True)
    app.MainLoop()

No comments:

Post a Comment