Search code examples
pythonconcurrencywxpython

How to prevent UI freeze in wxpython when running long task with concurrent future


I want to execute a long task in a wxpython UI, without the UI losing responsiveness.

I thought using concurrent futures with a ThreadPoolExecutor would allow me to do just that, but the UI still freezes.

Here is a simple code to reproduce the problem. The UI freezes for 5 seconds.

Why is this happening and how to solve?

import time
import wx
from concurrent.futures import ThreadPoolExecutor


class MyFrame(wx.Frame):
    def __init__(self, *args, **kwds):
        super().__init__(*args, **kwds)

        self.panel = wx.Panel(self, wx.ID_ANY)
        self.longtask_button = wx.Button(self, label="Long Task")
        self.longtask_button.Bind(wx.EVT_BUTTON, on_long_task)
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.longtask_button, 0, wx.ALIGN_RIGHT)
        self.SetSizer(sizer)
        self.Layout()


def on_long_task(event):
    with ThreadPoolExecutor() as executor:
        executor.submit(block5)


def block5():
    time.sleep(5)
    return 1


if __name__ == "__main__":
    app = wx.App()
    frame = MyFrame(None, wx.ID_ANY, "")
    frame.Show()
    app.MainLoop()

Solution

  • There are some issues in your code.

    def on_long_task(event):
        with ThreadPoolExecutor() as executor:
            executor.submit(block5)
    

    You create a ThreadPoolExecutor object inside the on_long_task function. When the function exits the ThreadPoolExecutor will be deleted because there is no more reference to it. It was just local to your function.

    You should create the ThreadPoolExecutor in the init function as member of MyFrame to keep a reference to it. With the reference you can check the if block5 is ready and get the result.

    Function on_long_task must be a member function of MyFrame to handle event so it should be

    def on_long_task(self, event):
    

    I have fixed your code and added a button to test if MyFrame stays responsive.

    import time
    import wx
    from concurrent.futures import ThreadPoolExecutor, Future
    
    def block5():
        time.sleep(5)
        return 1
    
    
    class MyFrame(wx.Frame):
    
        def __init__(self, *args, **kwds):
            super().__init__(*args, **kwds)
    
            self.panel = wx.Panel(self, wx.ID_ANY)
            self.longtask_button = wx.Button(self, label="Long Task")
            self.longtask_button.Bind(wx.EVT_BUTTON, self.on_long_task)
            self.longtask: Future = None
    
            self.response_button = wx.Button(self, label="RESPONSE TEST")
            self.response_button.Bind(wx.EVT_BUTTON, self.on_response_test)
            self.response_count = 0
    
            sizer = wx.BoxSizer(wx.VERTICAL)
            sizer.Add(self.longtask_button, 0, wx.ALIGN_RIGHT)
            self.SetSizer(sizer)
            self.Layout()
    
            self.executor = ThreadPoolExecutor()
    
        def on_long_task(self, event):
            self.longtask = self.executor.submit(block5)
            self.longtask_button.Enable(False)
    
        def on_response_test(self, event):
            if self.longtask:
                if self.longtask.done():
                    self.SetTitle(f'LONG TASK READY: {self.longtask.result()}')
                else:
                    self.response_count += 1
                    self.SetTitle(f'Response {self.response_count}')
    
    
    if __name__ == "__main__":
        app = wx.App()
        frame = MyFrame(None, wx.ID_ANY, "")
        frame.Show()
        app.MainLoop()