如何实现tkinter与asyncio的协同运行?
这个通用问题可归结为让tkinterawait协程函数的具体问题。
背景
若tkinter的事件循环被阻塞,循环会冻结直至阻塞函数返回,对应的GUI也会冻结。传统解决方案是将阻塞代码移至线程中。
新版asyncio模块可通过协程函数asyncio.to_thread(coro)调度线程调用,避免编写正确线程代码的难点。
基准示例:blocked.py
作为起点,我编写了一个基准程序(见下方代码)。它创建一个tkinter事件循环,尝试在2000ms后销毁自身并结束程序,但被一个运行4秒的阻塞函数阻止。程序输出如下:
08:51:57: Program started. 08:51:58: blocking_func started. 08:52:02: blocking_func completed. 08:52:02: Tk event loop terminated. 08:52:02: Program ended. Process finished with exit code 0
第一次尝试:async_blocked.py
阻塞代码被重构为协程函数,因此存在两个事件循环——tkinter和asyncio的事件循环。blocking_io_handler函数被调度到tkinter的事件循环并成功运行。协程函数blocking_func被调度到asyncio的事件循环并成功启动。
问题在于它要等到tkinter的事件循环终止后才启动。在协程函数main的整个执行过程中,asyncio的循环都是可用的,包括执行tk_root.mainloop()时。尽管如此,asyncio仍无法工作,因为tk_root.mainloop()执行期间没有通过await语句交出控制权,它必须等待后续执行的await asyncio.sleep(3)语句,而那时tkinter已停止运行。
此时,await表达式将控制权交还给异步循环3秒——足以启动运行4秒的blocking_func,但不足以让它完成。
08:38:22: Program started. 08:38:22: blocking_io_handler started. 08:38:22: blocking_io_handler completed. 08:38:24: Tk event loop terminated. 08:38:24: blocking_func started. 08:38:27: Program ended. Process finished with exit code 0
第二次尝试:asyncth_blocked.py
这段代码用协程函数asyncio.to_thread替换了asyncio.create_task函数,运行时出现警告:
07:26:46: Program started. 07:26:47: blocking_io_handler started. 07:26:47: blocking_io_handler completed. RuntimeWarning: coroutine 'to_thread' was never awaited asyncio.to_thread(blocking_func) RuntimeWarning: Enable tracemalloc to get the object allocation traceback 07:26:49: Tk event loop terminated. 07:26:49: Program ended. > Process finished with exit code 0
第三次尝试:asyncth_blocked_2.py
asyncio.to_thread是协程函数而非普通函数,必须被await:await asyncio.to_thread(blocking_func)。
由于await关键字在普通函数中是语法错误,def blocking_io_handler必须改为协程函数:async def blocking_io_handler。
这些修改体现在asyncth_blocked_2.py中,程序输出如下:
07:52:29: Program started. RuntimeWarning: coroutine 'blocking_io_handler' was never awaited func(*args) RuntimeWarning: Enable tracemalloc to get the object allocation traceback 07:52:31: Tk event loop terminated. 07:52:31: Program ended. Process finished with exit code 0
结论
要让tkinter与asyncio协同工作,必须将调度的函数调用tk_root.after(0, blocking_io_handler)转换为调度的协程函数调用,这是让asyncio循环有机会运行调度的异步任务的唯一方法。这是否可行?
代码
blocked.py
"""blocked.py""" import time import tkinter as tk def timestamped_msg(msg: str): print(f"{time.strftime('%X')}: {msg}") def blocking_func(): timestamped_msg('blocking_func started.') time.sleep(4) timestamped_msg('blocking_func completed.') def main(): timestamped_msg('Program started.') tk_root = tk.Tk() tk_root.after(0, blocking_func) tk_root.after(2000, tk_root.destroy) tk_root.mainloop() timestamped_msg('Tk event loop terminated.') timestamped_msg('Program ended.') if __name__ == '__main__': main()
async_blocked.py
"""async_blocked.py""" import asyncio import time import tkinter as tk def timestamped_msg(msg: str): print(f"{time.strftime('%X')}: {msg}") async def blocking_func(): timestamped_msg('blocking_func started.') await asyncio.sleep(4) timestamped_msg('blocking_func completed.') def blocking_io_handler(): timestamped_msg('blocking_io_handler started.') asyncio.create_task(blocking_func()) timestamped_msg('blocking_io_handler completed.') async def main(): timestamped_msg('Program started.') tk_root = tk.Tk() tk_root.after(0, blocking_io_handler) tk_root.after(2000, tk_root.destroy) tk_root.mainloop() timestamped_msg('Tk event loop terminated.') await asyncio.sleep(3) timestamped_msg('Program ended.') if __name__ == '__main__': asyncio.run(main())
asyncth_blocked.py
"""asyncth_blocked.py""" import asyncio import time import tkinter as tk def timestamped_msg(msg: str): print(f"{time.strftime('%X')}: {msg}") async def blocking_func(): timestamped_msg('blocking_func started.') await asyncio.sleep(4) timestamped_msg('blocking_func completed.') def blocking_io_handler(): timestamped_msg('blocking_io_handler started.') asyncio.to_thread(blocking_func) timestamped_msg('blocking_io_handler completed.') async def main(): timestamped_msg('Program started.') tk_root = tk.Tk() tk_root.after(0, blocking_io_handler) tk_root.after(2000, tk_root.destroy) tk_root.mainloop() timestamped_msg('Tk event loop terminated.') timestamped_msg('Program ended.') if __name__ == '__main__': asyncio.run(main())
asyncth_blocked_2.py
"""asyncth_blocked_2.py""" import asyncio import time import tkinter as tk def timestamped_msg(msg: str): print(f"{time.strftime('%X')}: {msg}") async def blocking_func(): timestamped_msg('blocking_func started.') await asyncio.sleep(4) timestamped_msg('blocking_func completed.') async def blocking_io_handler(): timestamped_msg('blocking_io_handler started.') await asyncio.to_thread(blocking_func) timestamped_msg('blocking_io_handler completed.') async def main(): timestamped_msg('Program started.') tk_root = tk.Tk() tk_root.after(0, blocking_io_handler) tk_root.after(2000, tk_root.destroy) tk_root.mainloop() timestamped_msg('Tk event loop terminated.') timestamped_msg('Program ended.') if __name__ == '__main__': asyncio.run(main())
内容的提问来源于stack exchange,提问作者lemi57ssss

