在不冻结 GUI 的情况下一起使用 asyncio 和 Tkinter(或另一个 GUI 库)

新手上路,请多包涵

我想将 asynciotkinter GUI 结合使用。我是新来的 asyncio ,我对它的理解不是很详细。单击第一个按钮时,此处的示例将启动 10 个任务。该任务只是模拟使用 sleep() 工作几秒钟。

示例代码在 Python 3.6.4rc1 中运行良好。 但问题 是 GUI 被冻结了。当我按下第一个按钮并启动 10 个异步任务时,在完成所有任务之前,我无法按下 GUI 中的第二个按钮。 GUI 永远不应该冻结——这是我的目标。

 #!/usr/bin/env python3
# -*- coding: utf-8 -*-

from tkinter import *
from tkinter import messagebox
import asyncio
import random

def do_freezed():
    """ Button-Event-Handler to see if a button on GUI works. """
    messagebox.showinfo(message='Tkinter is reacting.')

def do_tasks():
    """ Button-Event-Handler starting the asyncio part. """
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(do_urls())
    finally:
        loop.close()

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__':
    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()

    root.mainloop()

一个_side问题

…是因为这个错误,我无法再次运行该任务。

 Exception in Tkinter callback
Traceback (most recent call last):
  File "/usr/lib/python3.6/tkinter/__init__.py", line 1699, in __call__
    return self.func(*args)
  File "./tk_simple.py", line 17, in do_tasks
    loop.run_until_complete(do_urls())
  File "/usr/lib/python3.6/asyncio/base_events.py", line 443, in run_until_complete
    self._check_closed()
  File "/usr/lib/python3.6/asyncio/base_events.py", line 357, in _check_closed
    raise RuntimeError('Event loop is closed')
RuntimeError: Event loop is closed

多线程

多线程是一个可能的解决方案吗?只有两个线程 - 每个循环都有自己的线程?

编辑:在审查了这个问题和它与几乎所有 GUI 库(例如 PygObject/Gtk、wxWidgets、Qt 等)相关的答案之后。

原文由 buhtz 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 1.8k
2 个回答

在对您的代码稍作修改后,我在主线程中创建了 asyncio event_loop 并将其作为参数传递给 asyncio 线程。现在,在获取 url 时 Tkinter 不会冻结。

 from tkinter import *
from tkinter import messagebox
import asyncio
import threading
import random

def _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)

原文由 bhaskarc 发布,翻译遵循 CC BY-SA 4.0 许可协议

试图同时运行两个事件循环是一个可疑的提议。但是,由于 root.mainloop 只是重复调用 root.update,因此可以通过将 update 作为异步任务重复调用来模拟 mainloop。这是一个这样做的测试程序。我假设将 asyncio 任务添加到 tkinter 任务中会起作用。我检查过它仍然可以在 3.7.0a2 上运行。

 """Proof of concept: integrate tkinter, asyncio and async iterator.

Terry Jan Reedy, 2016 July 25
"""

import asyncio
from random import randrange as rr
import tkinter as tk

class 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, color

loop = asyncio.get_event_loop()
app = App(loop)
loop.run_forever()
loop.close()

随着时间间隔的减少,tk 更新开销和时间分辨率都会增加。对于 gui 更新,而不是动画,每秒 20 次可能就足够了。

我最近成功地运行了包含 tkinter 调用的 async def 协程并等待 mainloop。原型使用 asyncio Tasks 和 Futures,但我不知道添加普通的 asyncio 任务是否可行。如果想同时运行 asyncio 和 tkinter 任务,我认为使用 asyncio 循环运行 tk update 是一个更好的主意。

编辑:至少如上所用,没有 async def 协程的异常会杀死协程,但在某处被捕获并丢弃。静默错误非常令人讨厌。

EDIT2: https ://bugs.python.org/issue27546 上的其他代码和评论

原文由 Terry Jan Reedy 发布,翻译遵循 CC BY-SA 4.0 许可协议

推荐问题