web-dev-qa-db-fra.com

Utilisez asyncio et Tkinter (ou une autre bibliothèque GUI) ensemble sans geler l'interface graphique

Je veux utiliser asyncio en combinaison avec une interface graphique tkinter . Je suis nouveau dans asyncio et ma compréhension n'est pas très détaillée. L'exemple ici démarre 10 tâches en cliquant sur le premier bouton. La tâche simule simplement le travail avec une sleep() pendant quelques secondes.

L'exemple de code fonctionne correctement avec Python 3.6.4rc1. Mais le problème est que l'interface graphique est gelée. Lorsque j'appuie sur le premier bouton et démarre les 10 tâches asynchrones, je ne peux pas appuyer sur le deuxième bouton dans l'interface graphique tant que toutes les tâches ne sont pas terminées. L'interface graphique ne doit jamais geler - c'est mon objectif.

#!/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()

Un _side problem

... est que je ne suis pas en mesure d'exécuter la tâche une deuxième fois en raison de cette erreur.

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

Multithreading

Le multithreading serait-il une solution possible? Seulement deux threads - chaque boucle a son propre thread?

[~ # ~] modifier [~ # ~] : Après avoir examiné cette question et les réponses, elle est liée à presque toutes les bibliothèques GUI (par exemple PygObject/Gtk , wxWidgets, Qt, ...).

10
buhtz

Dans une légère modification de votre code, j'ai créé l'asyncio event_loop dans le thread principal et l'a passé en argument au thread asyncio. Maintenant, Tkinter ne gèle pas pendant que les URL sont récupérées.

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

Essayer d'exécuter les deux boucles d'événements en même temps est une proposition douteuse. Cependant, puisque root.mainloop appelle simplement root.update à plusieurs reprises, on peut simuler mainloop en appelant update à plusieurs reprises en tant que tâche asyncio. Voici un programme de test qui le fait. Je suppose que l'ajout de tâches asynchrones aux tâches tkinter fonctionnerait. J'ai vérifié qu'il fonctionne toujours avec 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()

La surcharge de mise à jour tk et la résolution temporelle augmentent à mesure que l'intervalle diminue. Pour les mises à jour de l'interface graphique, par opposition aux animations, 20 par seconde peuvent suffire.

J'ai récemment réussi à exécuter des coroutines async def contenant des appels tkinter et attend avec mainloop. Le prototype utilise des tâches et des contrats à terme asyncio, mais je ne sais pas si l'ajout de tâches asyncio normales fonctionnerait. Si l'on veut exécuter des tâches asyncio et tkinter ensemble, je pense que l'exécution de la mise à jour tk avec une boucle asyncio est une meilleure idée.

EDIT: au moins comme utilisé ci-dessus, exception sans async def coroutines tuer la coroutine mais sont quelque part capturés et jetés. Les erreurs silencieuses sont assez odieuses.

5
Terry Jan Reedy

Je suis un peu en retard à la fête, mais si vous ne ciblez pas Windows, vous pouvez utiliser aiotkinter pour obtenir ce que vous voulez. J'ai modifié votre code pour vous montrer comment utiliser ce package:

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

J'ai eu beaucoup de chance en exécutant une boucle d'E/S sur un autre thread, démarrée au début de la création de l'application et en y jetant des tâches à l'aide de asyncio.run_coroutine_threadsafe(..).

Je suis un peu surpris de pouvoir apporter des modifications aux widgets tkinter sur l'autre boucle/thread asyncio, et c'est peut-être un hasard que cela fonctionne pour moi - mais cela fonctionne.

Notez que pendant que les tâches asynchrones sont en cours, le bouton autre est toujours actif et répond. J'aime toujours la fonction désactiver/activer sur l'autre bouton afin de ne pas déclencher plusieurs tâches accidentellement, mais c'est juste une chose d'interface utilisateur.

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

Vous pouvez garder l'interface graphique vivante après avoir appuyé sur Button en ajoutant un appel à root.update_idletasks() au bon endroit:

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