Search code examples
pythontkinterpython-asynciopython-multithreading

Using async and threading inside Tkinter


I'm still a newbie to threading and asyncio in Python.

I am having a tkinter frame that is supposed to stop on a stop-button click. While the frame is open, two async functions are supposed to run in the background, where one is the producer loop and the other is the consumer loop (connected through an async queue).

Everything is working so far, though I'm getting an error when pressing the stop-button. I believe I am not quitting the coros properly.

Any help is much appreciated!

import random
import threading
import tkinter as tk
import asyncio


class ControlWindow(tk.Frame):

    def __init__(self, window=None):
        super().__init__()

        self.window = window

        tk.Button(window, text="Stop", command=self.stop).pack()

        self.thread = None
        self.tasks = None
        self.flag = False
        self.run()
        # self.window.mainloop()

    def schedule_check(self):
        self.window.after(811, self.check)

    def check(self):
        if self.thread.is_alive():
            self.schedule_check()

    def run(self):
        self.thread = threading.Thread(target=asyncio.run, args=[self.main_runner()])
        self.thread.start()
        self.schedule_check()

    async def main_runner(self):
        queue = asyncio.Queue()
        self.tasks = [asyncio.create_task(self.code(queue)), asyncio.create_task(self.handle_code(queue))]
        await asyncio.gather(*self.tasks)

    async def code(self, queue):
        while not self.flag:
            ran = random.randint(0, 5)
            queue.put_nowait(ran)
            await asyncio.sleep(1)

    async def handle_code(self, queue):
        while True:
            res = await queue.get()
            print(res)

    def stop(self):
        self.flag = True
        for task in self.tasks:
            task.cancel()
        self.thread.join()
        self.window.quit()


if __name__ == "__main__":
    root = tk.Tk()
    ControlWindow(root)
    root.mainloop()

Solution

  • This seems to work, i.e. using an Event for the quit flag:

    import random
    import threading
    import tkinter as tk
    import asyncio
    
    
    class AsyncThing:
        def __init__(self):
            self.quit_event = asyncio.Event()
            self.tasks = []
    
        def stop(self):
            self.quit_event.set()
            for task in self.tasks:
                task.cancel()
    
        async def main_runner(self):
            queue = asyncio.Queue()
            self.tasks = [
                asyncio.create_task(self.code(queue)),
                asyncio.create_task(self.handle_code(queue)),
            ]
            try:
                await asyncio.gather(*self.tasks)
            except asyncio.CancelledError:
                pass
    
        async def code(self, queue):
            while not self.quit_event.is_set():
                ran = random.randint(0, 5)
                queue.put_nowait(ran)
                await asyncio.sleep(1)
    
        async def handle_code(self, queue: asyncio.Queue):
            while not self.quit_event.is_set():
                res = await queue.get()
                print(res)
    
    
    class ControlWindow(tk.Frame):
        def __init__(self, window=None):
            super().__init__()
            self.window = window
            tk.Button(window, text="Stop", command=self.stop).pack()
            self.thing = AsyncThing()
            self.thread = None
            self.run()
    
        def run(self):
            self.thread = threading.Thread(target=asyncio.run, args=[self.thing.main_runner()])
            self.thread.start()
    
        def stop(self):
            self.thing.stop()
            self.thread.join()
            self.window.quit()
    
    
    if __name__ == "__main__":
        root = tk.Tk()
        ControlWindow(root)
        root.mainloop()