一起使用asyncio和Tkinter而不冻结GUI

buh*_*htz 7 python user-interface asynchronous tkinter python-asyncio

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

Python的示例代码运行良好3.6.4rc1.但问题是GUI被冻结了.当我按下第一个按钮并启动10个asyncio任务时,我无法按下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()
Run Code Online (Sandbox Code Playgroud)

一个_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
Run Code Online (Sandbox Code Playgroud)

多线程

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

bha*_*arc 12

在对您的代码稍作修改后,我event_loop在主线程中创建了 asyncio并将其作为参数传递给 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)
Run Code Online (Sandbox Code Playgroud)

  • 在主线程而不是工作线程中调用 asyncio.get_event_loop() 的原因是什么? (3认同)
  • @bhaskarc 它可能会让将来看到这一点的人感到困惑。最好只使用“Button(...).pack()”。很多新人都会犯这个错误。 (2认同)

Ter*_*edy 11

试图同时运行两个事件循环是一个可疑的命题.但是,由于root.mainloop只是重复调用root.update,因此可以通过重复调用update作为asyncio任务来模拟mainloop.这是一个测试程序.我认为向tkinter任务添加asyncio任务会起作用.我检查它仍然以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()
Run Code Online (Sandbox Code Playgroud)

随着间隔减小,tk更新开销和时间分辨率都增加.对于gui更新,与动画相反,每秒20个就足够了.

我最近成功运行了包含tkinter调用的异步def协程,并等待了mainloop.原型使用asyncio Tasks和Futures,但我不知道添加正常的asyncio任务是否有效.如果想要一起运行asyncio和tkinter任务,我认为使用asyncio循环运行tk update是一个更好的主意.

编辑:至少如上所述,没有异步def协同程序的异常会杀死协程,但会被捕获并丢弃.沉默的错误非常令人讨厌.

  • 至少目前,我把它留给你去试验。 (3认同)
  • 答案确实对其他人有帮助。 (2认同)
  • 另一个更新:我想我已经将这个内存问题缩小到 MacO 附带的 Tk 8.5 版本。升级到 8.6 修复了该问题。不幸的是,如果您使用自制程序安装 Python3,则获得升级的 Tk 以连接到 Python 是非常棘手的。如果您从 Python.org 安装 Python3,您将获得正确的东西。 (2认同)

out*_*ime 1

我用 解决了类似的任务multiprocessing

主要零件:

  1. 主要流程是Tk的流程mainloop
  2. daemon=True进程与执行命令aiohttp的服务。
  3. 使用双工对讲Pipe,因此每个进程都可以使用它的一端。

此外,我正在制作 Tk 的虚拟事件来简化应用程序端的按摩跟踪。您需要手动应用补丁。您可以检查python 的错误跟踪器以获取详细信息。

Pipe每 0.25 秒检查一次两侧。

$ python --version
Python 3.7.3
Run Code Online (Sandbox Code Playgroud)

主要.py

import asyncio
import multiprocessing as mp

from ws import main
from app import App


class WebSocketProcess(mp.Process):

    def __init__(self, pipe, *args, **kw):
        super().__init__(*args, **kw)
        self.pipe = pipe

    def run(self):
        loop = asyncio.get_event_loop()
        loop.create_task(main(self.pipe))
        loop.run_forever()


if __name__ == '__main__':
    pipe = mp.Pipe()
    WebSocketProcess(pipe, daemon=True).start()
    App(pipe).mainloop()
Run Code Online (Sandbox Code Playgroud)

应用程序.py

import tkinter as tk


class App(tk.Tk):

    def __init__(self, pipe, *args, **kw):
        super().__init__(*args, **kw)
        self.app_pipe, _ = pipe
        self.ws_check_interval = 250;
        self.after(self.ws_check_interval, self.ws_check)

    def join_channel(self, channel_str):
        self.app_pipe.send({
            'command': 'join',
            'data': {
                'channel': channel_str
            }
        })

    def ws_check(self):
        while self.app_pipe.poll():
            msg = self.app_pipe.recv()
            self.event_generate('<<ws-event>>', data=json.dumps(msg), when='tail')
        self.after(self.ws_check_interval, self.ws_check)
Run Code Online (Sandbox Code Playgroud)

ws.py

import asyncio

import aiohttp


async def read_pipe(session, ws, ws_pipe):
    while True:
        while ws_pipe.poll():
            msg = ws_pipe.recv()

            # web socket send
            if msg['command'] == 'join':
                await ws.send_json(msg['data'])

            # html request
            elif msg['command'] == 'ticker':
                async with session.get('https://example.com/api/ticker/') as response:
                    ws_pipe.send({'event': 'ticker', 'data': await response.json()})

        await asyncio.sleep(.25)


async def main(pipe, loop):
    _, ws_pipe = pipe
    async with aiohttp.ClientSession() as session:
        async with session.ws_connect('wss://example.com/') as ws:
            task = loop.create_task(read_pipe(session, ws, ws_pipe))
            async for msg in ws:
                if msg.type == aiohttp.WSMsgType.TEXT:
                    if msg.data == 'close cmd':
                        await ws.close()
                        break
                    ws_pipe.send(msg.json())
                elif msg.type == aiohttp.WSMsgType.ERROR:
                    break
Run Code Online (Sandbox Code Playgroud)