1
\$\begingroup\$

This is an attempt to create a usable alternative to the "normal" method of implementing concurrency with tkinter. The "normal" method seems to be by pro-actively polling a result queue and using mainloop.after(), such as in this example.

My (attempted and perhaps naive) implementation uses a listener interface so that multiple consumers may register for results asynchronously. A separate manager thread polls a work queue - when work is added to the queue it is submitted to a threadpool (managed by the manager thread). The listener interface shown below is simplified, however in the full implementation work can be "tagged" and a consumer can register to specifically receive callbacks to a specific tag. This has the advantage of keeping everything related to threading completely separate from the GUI thread.

class ThreadPoolListener(ABC):

@abstractmethod
def on_async_result(self, future: Future):
    pass


class ThreadPoolManager:

def __init__(self, max_threads=2):
    self.max_threads = max_threads
    self.work_queue = Queue()
    self.queue_lock = threading.Lock()
    self._pool = None
    self._listeners = []
    self.threaded_manager = threading.Thread(name='manager', args=(self.callback,), target=self.manager)
    self.threaded_manager.start()

def manager(self, callback):
    with concurrent.futures.ThreadPoolExecutor(max_workers=self.max_threads) as self._pool:
        while True:
            try:
                with self.queue_lock:
                    logger.info("Received lock")
                    task = self.work_queue.get_nowait()
                self._pool.submit(task).add_done_callback(callback)
            except queue.Empty:
                time.sleep(0.2)

def register_listener(self, listener: ThreadPoolListener):
    self._listeners.append(listener)

def unregister_listener(self, listener: ThreadPoolListener):
    self._listeners.remove(listener)

def callback(self, future):
    for listener in self._listeners:
        listener.on_async_result(future)

def add_task(self, func, *args, **kwargs):
    with self.queue_lock:
        self.work_queue.put(lambda: func(*args, **kwargs))

def shutdown(self):
    if self._pool:
        self._pool.shutdown()

While this code works, being new to concurrency in Python (though not in other languages) I would be very glad for any suggestions and crit regarding this implementation.

I am concerned that using "time.sleep" on the manager thread is a code smell, but I cannot think of an alternative.

Also, while keeping a threadpool active is not unusual in an application (it is expensive to start), I am not sure if this is the best solution since I am using tkinter as the GUI. Would it be better to start and stop the executor as necessary?

This will be utilised in a much more complex (currently synchronous) MVC application which does do a fair amount of network I/O (30% of the time) and image processing (mainly for generating image thumbnails, shown in the UI), however a simple example implementation is shown below:

class MyListener(ThreadPoolListener):
    def on_async_result(self, future):
        result = future.result()
        result_label.config(text=f"Task completed with result: {result}")


def simulate_task(task_id):
    time.sleep(2)  # Running some task
    return f"Task {task_id} result"


def add_task_to_thread_pool():
    task_id = task_counter[0]
    task_counter[0] += 1
    thread_pool.add_task(simulate_task, task_id)


root = tkinter.Tk()
root.title("ThreadPoolManager Example")

task_counter = [1]

frame = tkinter.Frame(root)
frame.pack(padx=20, pady=20)

start_button = tkinter.Button(frame, text="Add Task to Thread Pool", command=add_task_to_thread_pool)
start_button.pack()

result_label = tkinter.Label(frame, text="No tasks completed yet")
result_label.pack()

thread_pool = ThreadPoolManager(max_threads=2)
listener = MyListener()
thread_pool.register_listener(listener)

root.mainloop()

For this implementation I want to credit the following answers on stack exchange:

\$\endgroup\$

0

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.