Introduction
Sometimes, when your program has a task that takes a lot of time, like working with databases, web services, or complex calculations, you might want to let it happen in the background. This way, your program can keep running smoothly without waiting for the time-consuming task to finish. In Python, we can achieve this using threads. This article explores a straightforward example involving a virtual windowing system.
Implementation in Python
Before we start we need to import two packages:
import threading
import queue
The ResizeEvent
just consists of a new width and height, with their respective accessors:
class ResizeEvent:
_width: int = None
_height: int = None
def __init__(self, width: int, height: int):
self._width = width
self._height = height
@property
def width(self) -> int:
return self._width
@property
def height(self) -> int:
return self._height
Next we need a type to handle the events, this could also be used in more complex cases to filter events, or even transform. The ResizeEventHandler
has a function handler
which handles the event. In our case, the event is neither filtered nor transformed, but passed as it is to the handler:
class ResizeEventHandler:
_handler = None
def __init__(self, initial_handler):
self._handler = initial_handler
def handle(self, event: ResizeEvent):
return self._handler(event)
What good is a handler without a listener, so we will implement a ResizeEventListener
. This class basically has a queue of events and a reference to the handler.
The start()
method starts a thread which listens to the events queue and handles the events.
The stop()
method puts a None
value in the queue, thereby halting the thread.
In the send()
method an event is put to the queue to be handled.
Let’s look at the code:
class ResizeEventListener:
_events: queue.Queue = None
_handler: ResizeEventHandler = None
def __init__(self, init_handler):
self._events = queue.Queue()
self._handler = ResizeEventHandler(init_handler)
def start(self, initial_window):
def run():
while True:
event = self._events.get()
if event is None:
break
new_width, new_height, error = self._handler.handle(event)
if error:
break
initial_window.width = new_width
initial_window.height = new_height
threading.Thread(target=run).start()
def stop(self):
self._events.put(None)
def send(self, event: ResizeEvent):
self._events.put(event)
Next we need to define the Window
class. The Window
class has a title, width and height, but also a resize listener, which is initialized in the constructor.
The open()
method prints a message, and starts the listener, passing it a reference to the window itself.
The close()
method stops the listener and prints a message.
class Window:
_listener: ResizeEventListener = None
_title: str = None
_width: int = None
_height: int = None
def __init__(self, title: str, width: int, height: int, handler):
self._listener = ResizeEventListener(handler)
self._title = title
self._width = width
self._height = height
def open(self):
self._listener.start(self)
print(f"Window {self._title} opened with size {self._width}x{self._height}")
def close(self):
self._listener.stop()
print(f"Window {self._title} closed")
def resize(self, event: ResizeEvent):
self._listener.send(event)
@property
def width(self) -> int:
return self._width
@width.setter
def width(self, width: int):
self._width = width
@property
def height(self) -> int:
return self._height
@height.setter
def height(self, height: int):
self._height = height
Time to test
We will start our test by setting up a handler function, which we pass to the window constructor.
Then we open the window and resize it several times, before closing it, and printing out the final width and height.
if __name__ == '__main__':
def handler(event: ResizeEvent):
print(f"Window resized to {event.width}x{event.height}")
return event.width, event.height, None
window = Window("My Window", 800, 600, handler)
window.open()
window.resize(ResizeEvent(1024, 768))
window.resize(ResizeEvent(644, 484))
window.resize(ResizeEvent(800, 600))
window.close()
print(f"Height is {window.height}")
print(f"Width is {window.width}")
Conclusion
Using multithreading this way in Python was surprisingly simple, although it took me some experimentation to get things right. Possible enhancements could be to make the eventhandler more generic.
As you can see dispatching calculations to the background in Python can be both powerful and simple to implement. It is however fair to say that this is a deliberately simple example. In a next blog post I will discuss a somewhat more involved example.