M0AGX / LB9MG

Amateur radio and embedded systems

Thread-safe event passing in wxPython

wxWidgets are my favourite widget toolkit for making quick and dirty utilitarian interfaces in Python. The bindings are called wxPython. Why I like wxPython? I find Tkinter too basic, Qt needing too much boilerplate code, and GTK (PyGobject) too fiddly to set up on Windows (maybe it has since improved). While getting wxPython is as easy as pip install wxPython on all platforms.

Most applications run some kind of background tasks. If the whole application was single threaded the GUI would freeze until the background tasks complete so a common technique is to have separate threads for the GUI and the "backend". Multithreading naturally leads to the issue of synchronization. For example you should not destroy a button from one thread when it is being repainted by another thread.

Changing the GUI from other threads is asking for trouble. There are mechanisms to handle events when it is safe for the GUI. Even though CPython runs basically in a single thread (due to the GIL) all "foreign" libraries (like wxWidgets written in C++) can run in their own threads and use everything the OS provides. Violating thread-safety usually leads to outright crashes in the C++ libraries which are hard to diagnose from the Python end. There may not even be a Python exception raised. Just a pure crash.

My wxPython flow

I usually start an app by designing the static elements of the window in wxFormBuilder and autogenerating Python code. Then, I derive my own class from the autogenerated code to add custom things.

In almost every app GUI events have to go in two ways. From the GUI to the backend (worker threads) and from the backend to the GUI. Passing events from the GUI is easy as the callbacks can use standard Python queues (GUI puts data/events into the queue, worker thread reads the events from the queue).

The other direction (backend to GUI) is more tricky and differs between various widget toolkits.

Event container class

First step is to have a container class to represent the events:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import wx
class GuiEvent(wx.PyCommandEvent):
    """Event to signal that a Feed value is ready"""
    def __init__(self, etype, eid, payload):
        """Creates the event object"""
        wx.PyCommandEvent.__init__(self, etype, eid)
        self._payload = payload

    def get_payload(self):
        return self._payload

This is basically a wrapper. The payload can be anything. I prefer passing plain dictionaries because they are self-describing and because I am not concerned with performance (I don't pass that many events). Any data type could be used. Passing raw integers or tuples is also an option.

The GUI

This is a minimal example. The important parts are:

  • Registering the custom event handler (process_backend_event).
  • Processing the events in process_backend_event. This function runs when it is safe to modify the GUI.
  • The feed_event function that can be called from any thread. It simply passes the payload_dict to wx.PostEvent. This makes passing the event thread-safe.
  • Callbacks from GUI widgets put their own events into outbound_event_queue. This queue should be read by the backend thread.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#!/usr/bin/env python3
import queue
import wx
import gui_template # Generated by wxFormBuilder

# gui_template.frame_base comes from wxFormBuilder
class Gui_frame (gui_template.frame_base):

    def __init__(self, outbound_event_queue):
        super().__init__(None) # Run the original constructor

        self.outbound_event_queue = outbound_event_queue # GUI->backend queue

        self.myEVT_FEED = wx.NewEventType()
        self.EVT_FEED = wx.PyEventBinder(self.myEVT_FEED, 1)
        self.Bind(self.EVT_FEED, self.process_backend_event)

    # This function runs in GUI thread. It is safe to modify widgets from here.
    def process_backend_event(self, evt):
        payload = evt.get_payload()

        if payload['type'] == 'some_event_type1':
            self.do_some_stuff_type1(payload['stuff_data1'])
        elif payload['type'] == 'some_event_type2':
            self.do_some_stuff_type2(payload['stuff_data2'])

    def feed_event(self, payload_dict):
        wx.PostEvent(self, GuiEvent(self.myEVT_FEED, -1, payload_dict))

    def start_button_pressed(self, *unused_args):
        self.outbound_event_queue.put({'type': 'start_button'})

    def window_closed(self, *unused_args):
        self.outbound_event_queue.put({'type': 'exit'})
        self.Destroy()

# Window testing without the backend
if __name__ == "__main__":
    # Create a new app, don't redirect stdout/stderr to a window.
    app = wx.App(False)
    frame = Gui_frame(queue.Queue()) # Dummy queue
    frame.Show(True)
    app.MainLoop()