web-dev-qa-db-fra.com

Comment exécuter des tâches asynchrones dans Python Applications GObject Introspection

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?

16
David Planella

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é:

  1. La plupart des implémentations Python ont un GIL, ce qui permet aux threads de ne pas utiliser pleinement les multicœurs. Donc: n'utilisez pas de threads avec python!
  2. Les objets que vous souhaitez renvoyer dans 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!
  3. La bibliothèque que vous appelez (softwarecenter.db) n’est pas threadsafe (semble inclure gtk ou similaire). Par conséquent, l’appel de ces méthodes dans un thread entraîne un comportement étrange (dans mon test, tout va de "ça marche" au "core dump" quitter sans résultats). Donc: pas de threads avec softwarecenter.
  4. Tout rappel asynchrone dans gtk ne doit rien faire , à l'exception du renvoi d'un rappel qui sera appelé dans le mainloop de la glib. Donc: no print, aucun état gtk ne change, excepté l'ajout d'un rappel!
  5. Gtk et autres ne fonctionnent pas avec des threads prêts à l'emploi. Vous devez faire 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:

  1. Réécrivez votre slow_load pour renvoyer des résultats sélectionnables et utilisez des contrats à terme avec des processus.
  2. Passez de softwarecenter à python-apt ou similaire (vous n'aimez probablement pas cela). Mais depuis que vous êtes employé par Canonical, vous pouvez demander directement aux développeurs de softwarecenter d’ajouter à leur logiciel leur logiciel (par exemple, en indiquant qu’il n’est pas thread-safe) et même mieux, rendant softwarecenter threadsafe.

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.

15
xubuntix

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()
11

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

2
dobey

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

2
mhall119

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.

1
RobotHumans