Use asyncio and Tkinter (or another GUI lib) together without freezing the GUI
Trying to run both event loops at the same time is a dubious proposition. However, since root.mainloop simply calls root.update repeatedly, one can simulate mainloop by calling update repeatedly as an asyncio task. Here is a test program that does so. I presume adding asyncio tasks to the tkinter tasks would work. I checked that it still runs with 3.7.0a2.
"""Proof of concept: integrate tkinter, asyncio and async iterator.Terry Jan Reedy, 2016 July 25"""import asynciofrom random import randrange as rrimport tkinter as tkclass App(tk.Tk): def __init__(self, loop, interval=1/120): super().__init__() self.loop = loop self.protocol("WM_DELETE_WINDOW", self.close) self.tasks = [] self.tasks.append(loop.create_task(self.rotator(1/60, 2))) self.tasks.append(loop.create_task(self.updater(interval))) async def rotator(self, interval, d_per_tick): canvas = tk.Canvas(self, height=600, width=600) canvas.pack() deg = 0 color = 'black' arc = canvas.create_arc(100, 100, 500, 500, style=tk.CHORD, start=0, extent=deg, fill=color) while await asyncio.sleep(interval, True): deg, color = deg_color(deg, d_per_tick, color) canvas.itemconfigure(arc, extent=deg, fill=color) async def updater(self, interval): while True: self.update() await asyncio.sleep(interval) def close(self): for task in self.tasks: task.cancel() self.loop.stop() self.destroy()def deg_color(deg, d_per_tick, color): deg += d_per_tick if 360 <= deg: deg %= 360 color = '#%02x%02x%02x' % (rr(0, 256), rr(0, 256), rr(0, 256)) return deg, colorloop = asyncio.get_event_loop()app = App(loop)loop.run_forever()loop.close()
Both the tk update overhead and time resolution increase as the interval is decreased. For gui updates, as opposed to animations, 20 per second may be enough.
I recently succeeded in running async def coroutines containing tkinter calls and awaits with mainloop. The prototype uses asyncio Tasks and Futures, but I don't know if adding normal asyncio tasks would work. If one wants to run asyncio and tkinter tasks together, I think running tk update with an asyncio loop is a better idea.
EDIT: A least as used above, exception without async def coroutines kill the coroutine but are somewhere caught and discarded. Silent error are pretty obnoxious.
In a slight modification to your code, I created the asyncio event_loop
in the main thread and passed it as an argument to the asyncio thread. Now Tkinter won't freeze while the urls are fetched.
from tkinter import *from tkinter import messageboximport asyncioimport threadingimport randomdef _asyncio_thread(async_loop): async_loop.run_until_complete(do_urls())def do_tasks(async_loop): """ Button-Event-Handler starting the asyncio part. """ threading.Thread(target=_asyncio_thread, args=(async_loop,)).start() async def one_url(url): """ One task. """ sec = random.randint(1, 8) await asyncio.sleep(sec) return 'url: {}\tsec: {}'.format(url, sec)async def do_urls(): """ Creating and starting 10 tasks. """ tasks = [one_url(url) for url in range(10)] completed, pending = await asyncio.wait(tasks) results = [task.result() for task in completed] print('\n'.join(results))def do_freezed(): messagebox.showinfo(message='Tkinter is reacting.')def main(async_loop): root = Tk() Button(master=root, text='Asyncio Tasks', command= lambda:do_tasks(async_loop)).pack() Button(master=root, text='Freezed???', command=do_freezed).pack() root.mainloop()if __name__ == '__main__': async_loop = asyncio.get_event_loop() main(async_loop)
I'm a bit late to the party but if you are not targeting Windows you can use aiotkinter to achieve what you want. I modified your code to show you how to use this package:
from tkinter import *from tkinter import messageboximport asyncioimport randomimport aiotkinterdef do_freezed(): """ Button-Event-Handler to see if a button on GUI works. """ messagebox.showinfo(message='Tkinter is reacting.')def do_tasks(): task = asyncio.ensure_future(do_urls()) task.add_done_callback(tasks_done)def tasks_done(task): messagebox.showinfo(message='Tasks done.')async def one_url(url): """ One task. """ sec = random.randint(1, 15) await asyncio.sleep(sec) return 'url: {}\tsec: {}'.format(url, sec)async def do_urls(): """ Creating and starting 10 tasks. """ tasks = [ one_url(url) for url in range(10) ] completed, pending = await asyncio.wait(tasks) results = [task.result() for task in completed] print('\n'.join(results))if __name__ == '__main__': asyncio.set_event_loop_policy(aiotkinter.TkinterEventLoopPolicy()) loop = asyncio.get_event_loop() root = Tk() buttonT = Button(master=root, text='Asyncio Tasks', command=do_tasks) buttonT.pack() buttonX = Button(master=root, text='Freezed???', command=do_freezed) buttonX.pack() loop.run_forever()