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()
... 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
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, ...).
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)
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.
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()
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()
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()