Easy Python Event Async: Efficient Concurrent Programming Unveiled

Photo by Mehmet Turgut Kirkgoz : https://www.pexels.com/photo/wall-with-round-clocks-18075142/

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.

Leave a Reply

Your email address will not be published. Required fields are marked *