J'écris une application Python + GObject qui doit lire une quantité non négligeable de données à partir du disque au démarrage. Les données sont lues de manière synchrone et il faut environ 10 secondes pour terminer l'opération de lecture, période pendant laquelle le chargement de l'interface utilisateur est retardé.
J'aimerais exécuter la tâche de manière asynchrone et recevoir une notification lorsqu'elle est prête, sans bloquer l'interface utilisateur, plus ou moins comme:
def take_ages():
read_a_huge_file_from_disk()
def on_finished_long_task():
print "Finished!"
run_long_task(task=take_ages, callback=on_finished_long_task)
load_the_UI_without_blocking_on_long_task()
J'ai utilisé GTask dans le passé pour ce genre de chose, mais je crains que son code n'ait pas été modifié depuis 3 ans, et encore moins été porté à GObject Introspection. Plus important encore, il n'est plus disponible dans Ubuntu 12.04. Je cherche donc un moyen facile d'exécuter des tâches de manière asynchrone, soit de manière standard Python, soit de manière standard GObject/GTK +.
Edit: voici du code avec un exemple de ce que j'essaie de faire. J'ai essayé python-defer
comme suggéré dans les commentaires, mais je n'ai pas réussi à exécuter la tâche longue de manière asynchrone et à laisser la charge de l'interface utilisateur sans attendre la fin de celle-ci. Parcourir le code de test .
Existe-t-il un moyen simple et largement utilisé d’exécuter des tâches asynchrones et d’être averti de leur exécution?
Votre problème est très courant, il existe donc une multitude de solutions (hangars, files d'attente avec multitraitement ou threading, pools de travailleurs, ...)
Comme il est si courant, il existe également une solution intégrée python (en 3.2, mais reportée ici: http://pypi.python.org/pypi/futures ) appelé concurrent.futures. Les 'Futures' étant disponibles dans de nombreuses langues, python les appelle de la même manière. Voici les appels typiques (et voici votre exemple complet , cependant, la partie db est remplacée par sleep, voyez ci-dessous pourquoi).
from concurrent import futures
executor = futures.ProcessPoolExecutor(max_workers=1)
#executor = futures.ThreadPoolExecutor(max_workers=1)
future = executor.submit(slow_load)
future.add_done_callback(self.on_complete)
Passons maintenant à votre problème, qui est beaucoup plus compliqué que ne le suggère votre exemple. En général, vous avez des threads ou des processus pour résoudre ce problème, mais voici pourquoi votre exemple est si compliqué:
slow_load
à partir de la base de données ne peuvent pas être choisis, ce qui signifie qu'ils ne peuvent pas simplement être transmis entre les processus. Donc: pas de multitraitement avec les résultats du centre logiciel!print
, aucun état gtk ne change, excepté l'ajout d'un rappel!threads_init
, et si vous appelez une méthode gtk ou similaire, vous devez protéger cette méthode (dans les versions précédentes, il s'agissait de gtk.gdk.threads_enter()
, gtk.gdk.threads_leave()
. Voir par exemple gstreamer: http://pygstdocs.berlios.de /pygst-tutorial/playbin.html ).Je peux vous donner la suggestion suivante:
slow_load
pour renvoyer des résultats sélectionnables et utilisez des contrats à terme avec des processus.Remarque: les solutions données par les autres (Gio.io_scheduler_Push_job
, async_call
) fonctionnent avec avec time.sleep
mais pas avec softwarecenter.db
. Ceci est dû au fait que tout se résume à des threads ou processus et que les threads ne fonctionnent pas avec gtk et softwarecenter
.
Voici une autre option utilisant le planificateur d'E/S de GIO (je ne l'avais jamais utilisé auparavant depuis Python, mais l'exemple ci-dessous semble fonctionner correctement).
from gi.repository import GLib, Gio, GObject
import time
def slow_stuff(job, cancellable, user_data):
print "Slow!"
for i in xrange(5):
print "doing slow stuff..."
time.sleep(0.5)
print "finished doing slow stuff!"
return False # job completed
def main():
GObject.threads_init()
print "Starting..."
Gio.io_scheduler_Push_job(slow_stuff, None, GLib.PRIORITY_DEFAULT, None)
print "It's running async..."
GLib.idle_add(ui_stuff)
GLib.MainLoop().run()
def ui_stuff():
print "This is the UI doing stuff..."
time.sleep(1)
return True
if __== '__main__':
main()
Utilisez l'API introspected Gio
pour lire un fichier, avec ses méthodes asynchrones, et lors de l'appel initial, faites-la en tant que délai d'attente avec GLib.timeout_add_seconds(3, call_the_gio_stuff)
où call_the_gio_stuff
est une fonction qui renvoie False
.
Il est nécessaire d’ajouter la temporisation ici (un nombre de secondes différent peut toutefois être nécessaire), car, bien que les appels asynchrones Gio soient asynchrones, ils ne sont pas non bloquants, ce qui signifie que la lecture d’un fichier volumineux nombre de fichiers, peut entraîner le blocage de l’UI, l’UI et les E/S se trouvant toujours dans le même fil (principal).
Si vous souhaitez écrire vos propres fonctions pour qu'elles soient asynchrones et s'intégrer à la boucle principale, à l'aide des API d'entrée-sortie de fichier de Python, vous devrez écrire le code en tant qu'objet GObject, passer des rappels ou utiliser python-defer
pour vous aider. Tu le fais. Mais il est préférable d’utiliser Gio ici, car il peut vous apporter de nombreuses fonctionnalités de Nice, en particulier si vous ouvrez des fichiers ouverts/sauvegardés dans l’UX.
Vous pouvez également utiliser GLib.idle_add (rappel) pour appeler la tâche d'exécution longue une fois que GLib Mainloop a terminé tous ses événements de priorité supérieure (ce qui inclut, je crois, la construction de l'interface utilisateur).
Je pense qu'il convient de noter qu'il s'agit d'une manière compliquée de faire ce que @mhall a suggéré.
Essentiellement, vous devez exécuter ceci puis exécuter la fonction async_call.
Si vous voulez voir comment cela fonctionne, vous pouvez jouer avec la minuterie d'arrêt et continuer à cliquer sur le bouton. C'est essentiellement la même chose que la réponse de @ mhall sauf qu'il y a un exemple de code.
Basé sur ceci qui n'est pas mon travail.
import threading
import time
from gi.repository import Gtk, GObject
# calls f on another thread
def async_call(f, on_done):
if not on_done:
on_done = lambda r, e: None
def do_call():
result = None
error = None
try:
result = f()
except Exception, err:
error = err
GObject.idle_add(lambda: on_done(result, error))
thread = threading.Thread(target = do_call)
thread.start()
class SlowLoad(Gtk.Window):
def __init__(self):
Gtk.Window.__init__(self, title="Hello World")
GObject.threads_init()
self.connect("delete-event", Gtk.main_quit)
self.button = Gtk.Button(label="Click Here")
self.button.connect("clicked", self.on_button_clicked)
self.add(self.button)
self.file_contents = 'Slow load pending'
async_call(self.slow_load, self.slow_complete)
def on_button_clicked(self, widget):
print self.file_contents
def slow_complete(self, results, errors):
'''
'''
self.file_contents = results
self.button.set_label(self.file_contents)
self.button.show_all()
def slow_load(self):
'''
'''
time.sleep(5)
self.file_contents = "Slow load in progress..."
time.sleep(5)
return 'Slow load complete'
if __== '__main__':
win = SlowLoad()
win.show_all()
#time.sleep(10)
Gtk.main()
Remarque supplémentaire, vous devez laisser l'autre thread finir avant qu'il ne se termine correctement ou rechercher un fichier.lock dans votre thread enfant.
Modifier pour adresser un commentaire:
Au départ, j'ai oublié GObject.threads_init()
. Évidemment, lorsque le bouton s'est déclenché, il a initialisé le threading pour moi. Cela masquait l'erreur pour moi.
Généralement, le flux est créer la fenêtre en mémoire, lancer immédiatement l'autre thread, lorsque le thread est terminé, mettre à jour le bouton. J'ai ajouté un sommeil supplémentaire avant même d'appeler Gtk.main pour vérifier que la mise à jour complète POURRAIT être exécutée avant même que la fenêtre ne soit dessinée. Je l'ai également commenté pour vérifier que le lancement du fil de discussion n'empêchait pas le dessin de la fenêtre.