web-dev-qa-db-ja.com

GUIをフリーズせずにasyncioとTkinter(または別のGUIライブラリ)を一緒に使用する

asyncio GUIと組み合わせて tkinter を使用したい。私はasyncioを初めて利用しましたが、それについての理解はあまり詳しくありません。この例では、最初のボタンをクリックすると10個のタスクが開始されます。タスクは、数秒間sleep()を使用して作業をシミュレートしているだけです。

サンプルコードはPython 3.6.4rc1しかし問題は、GUIがフリーズすることです。最初のボタンを押して10個の非同期タスクを開始すると、すべてのタスクが完了するまでGUIの2番目のボタンを押すことができません。 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問題

...このエラーのため、2回目にタスクを実行できません。

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

マルチスレッド

マルチスレッディングは可能な解決策でしょうか? 2つのスレッドのみ-各ループには独自のスレッドがありますか?

[〜#〜] edit [〜#〜]:この質問と回答を確認した後、ほぼすべてのGUIライブラリ(PygObject/Gtkなど)に関連しています、wxWidgets、Qt、...)。

10
buhtz

コードを少し変更して、メインスレッドに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()
    buttonX = Button(master=root, text='Freezed???', command=do_freezed).pack()
    root.mainloop()

if __== '__main__':
    async_loop = asyncio.get_event_loop()
    main(async_loop)
3
bhaskarc

両方のイベントループを同時に実行しようとすると、疑わしい提案になります。ただし、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()

間隔を短くすると、tk更新のオーバーヘッドと時間解決の両方が増加します。 GUIの更新では、アニメーションとは異なり、1秒あたり20で十分です。

私は最近、tkinter呼び出しを含むasync defコルーチンの実行に成功し、mainloopで待機します。プロトタイプはasyncioタスクと先物を使用しますが、通常のasyncioタスクを追加することが機能するかどうかはわかりません。 asyncioタスクとtkinterタスクを一緒に実行したい場合は、asyncioループを使用してtk updateを実行することをお勧めします。

編集:少なくとも上記で使用されているように、非同期defコルーチンのない例外はコルーチンを強制終了しますが、どこかでキャッチおよび破棄されます。サイレントエラーはかなり厄介です。

5
Terry Jan Reedy

パーティーには少し遅れますが、Windowsをターゲットにしていない場合は、 aiotkinter を使用して目的を達成できます。このパッケージの使用方法を示すようにコードを変更しました。

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

import aiotkinter

def 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()
1
Winston

私は別のスレッドでI/Oループを実行し、アプリの作成の最初から開始し、asyncio.run_coroutine_threadsafe(..)を使用してタスクを投げることができました。

他のasyncioループ/スレッドのtkinterウィジェットに変更を加えることができるのに、私はちょっと驚いています。おそらく、それが私のために機能しているのはひどいことですが、機能します。

Asyncioタスクが発生している間、otherボタンはまだ有効で応答しています。私は常に他のボタンを無効/有効にするのが好きなので、誤って複数のタスクを起動しないようにしますが、それは単なるUIです。

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


# Please wrap all this code in a Nice App class, of course

def _run_aio_loop(loop):
    asyncio.set_event_loop(loop)
    loop.run_forever()
aioloop = asyncio.new_event_loop()
t = threading.Thread(target=partial(_run_aio_loop, aioloop))
t.daemon = True  # Optional depending on how you plan to shutdown the app
t.start()

buttonT = None

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. """
    buttonT.configure(state=DISABLED)
    asyncio.run_coroutine_threadsafe(do_urls(), aioloop)

async def one_url(url):
    """ One task. """
    sec = random.randint(1, 3)
    # root.update_idletasks()  # We can delete this now
    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(3)]
    completed, pending = await asyncio.wait(tasks)
    results = [task.result() for task in completed]
    print('\n'.join(results))
    buttonT.configure(state=NORMAL)  # Tk doesn't seem to care that this is called on another thread


if __== '__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()
0
rharder

Buttonを押した後、適切な場所にroot.update_idletasks()への呼び出しを追加することにより、GUIを存続させることができます。

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)
    root.update_idletasks()  # ADDED: Allow tkinter to update gui.
    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 __== '__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()
0
martineau