【问题标题】:Using async/await keywords with Tk.after() method of tkinter在 tkinter 的 Tk.after() 方法中使用 async/await 关键字
【发布时间】:2018-10-02 03:32:06
【问题描述】:

我正在使用 Python3.5 和 Tkinter 创建一个加密货币交换 API 客户端。我有几个显示器,我想每 10 秒异步更新一次。我可以使用 Tk.after() 每 10 秒更新一次显示,就像在这个例子中一样

def updateLoans():
    offers = dd.loanOffers()
    demands = dd.loanDemands()
    w.LoanOfferView.delete(1.0, END)
    w.LoanDemandView.delete(1.0, END)
    w.LoanOfferView.insert(END, offers)
    w.LoanDemandView.insert(END, demands)
    print('loans refreshed')

    root.after(10000, updateLoans)

为了让after 方法每 10 秒持续更新一次,函数 updateLoans() 需要作为可调用对象传递到函数内部的 after()

现在困扰我的部分是,当我使这个函数与 python 的新 async 和 await 关键字异步时

async def updateLoans():
    offers = await dd.loanOffers()
    demands = await dd.loanDemands()
    w.LoanOfferView.delete(1.0, END)
    w.LoanDemandView.delete(1.0, END)
    w.LoanOfferView.insert(END, offers)
    w.LoanDemandView.insert(END, demands)
    print('loans refreshed')

    root.after(10000, updateLoans)

这里的问题是我不能等待 after 方法的参数内部的可调用对象。所以我得到一个运行时警告。 RuntimeWarning: coroutine 'updateLoans' was never awaited.

我的初始函数调用 IS 放置在事件循环内。

loop = asyncio.get_event_loop()
loop.run_until_complete(updateLoans())
loop.close()

显示最初填充得很好,但永远不会更新。

如何使用Tk.after 异步持续更新 tkinter 显示?

【问题讨论】:

    标签: python python-3.x tkinter python-3.5 python-asyncio


    【解决方案1】:

    tk.after 接受普通函数,而不是协程。要运行协程完成,您可以使用run_until_complete,就像您第一次一样:

    loop = asyncio.get_event_loop()
    root.after(10000, lambda: loop.run_until_complete(updateLoans()))
    

    另外,不要调用loop.close(),因为您将再次需要循环。


    上述快速修复适用于许多用例。然而,事实是,如果updateLoans() 由于网络缓慢或远程服务问题而需要很长时间,它将使 GUI 完全无响应。一个好的 GUI 应用程序会希望避免这种情况。

    虽然 Tkinter 和 asyncio cannot share an event loop yet,但完全可以在单独的线程中运行 asyncio 事件循环。然后主线程运行 GUI,而专用 asyncio 线程运行所有 asyncio 协程。当事件循环需要通知 GUI 刷新某些东西时,它可以使用队列as shown here。另一方面,如果 GUI 需要告诉事件循环做某事,它可以调用call_soon_threadsafe or run_coroutine_threadsafe

    示例代码(未经测试):

    gui_queue = queue.Queue()
    
    async def updateLoans():
        while True:
            offers = await dd.loanOffers()
            demands = await dd.loanDemands()
            print('loans obtained')
            gui_queue.put(lambda: updateLoansGui(offers, demands))
            await asyncio.sleep(10)
    
    def updateLoansGui(offers, demands):
        w.LoanOfferView.delete(1.0, END)
        w.LoanDemandView.delete(1.0, END)
        w.LoanOfferView.insert(END, offers)
        w.LoanDemandView.insert(END, demands)
        print('loans GUI refreshed')
    
    # http://effbot.org/zone/tkinter-threads.htm
    def periodicGuiUpdate():
        while True:
            try:
                fn = gui_queue.get_nowait()
            except queue.Empty:
                break
            fn()
        root.after(100, periodicGuiUpdate)
    
    # Run the asyncio event loop in a worker thread.
    def start_loop():
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)
        loop.create_task(updateLoans())
        loop.run_forever()
    threading.Thread(target=start_loop).start()
    
    # Run the GUI main loop in the main thread.
    periodicGuiUpdate()
    root.mainloop()
    
    # To stop the event loop, call loop.call_soon_threadsafe(loop.stop).
    # To start a coroutine from the GUI, call asyncio.run_coroutine_threadsafe.
    

    【讨论】:

    • 我收到的关于堆栈溢出的最清晰、最彻底的答案之一。感谢这帮助了很多
    • 所以尝试实施您的第二个解决方案,我不断收到此错误。 RuntimeError: There is no current event loop in thread 'Thread-1'
    • @RileyHughes 好的,在 Python 3.5 中,您需要为新线程额外设置事件循环。我现在修改了答案。
    猜你喜欢
    • 2015-05-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-11-28
    • 1970-01-01
    • 2023-03-27
    • 2018-04-08
    • 2017-10-03
    相关资源
    最近更新 更多