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


16

J'écris une application Python + GObject qui doit lire une quantité non triviale de données 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é.

Je voudrais 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é touché depuis 3 ans, et encore moins porté sur GObject Introspection. Plus important encore, il n'est plus disponible dans Ubuntu 12.04. Je recherche donc un moyen facile d'exécuter des tâches de manière asynchrone, soit de manière Python standard, 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-defercomme suggéré dans les commentaires, mais je n'ai pas réussi à exécuter la tâche longue de manière asynchrone et à laisser l'interface utilisateur se charger sans avoir à attendre qu'elle se termine. Parcourez le code de test .

Existe-t-il un moyen simple et largement utilisé d'exécuter des tâches asynchrones et d'être averti lorsqu'elles sont terminées?


Ce n'est pas un joli exemple, mais je suis sûr que c'est ce que vous recherchez: raw.github.com/gist/1132418/…
RobotHumans

Cool, je pense que votre async_callfonction pourrait être ce dont j'ai besoin. Pourriez-vous m'étendre un peu et ajouter une réponse, afin que je puisse l'accepter et vous créditer après l'avoir testée? Merci!
David Planella

1
Grande question, très utile! ;-)
Rafał Cieślak

Réponses:


15

Votre problème est très courant, donc il y a des tonnes de solutions (hangars, files d'attente avec multiprocessing ou threading, pools de travailleurs, ...)

Comme il est si courant, il existe également une solution intégrée de python (en 3.2, mais rétroportée ici: http://pypi.python.org/pypi/futures ) appelée concurrent.futures. Les 'Futures' sont disponibles dans de nombreuses langues, donc 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, voir 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 simple 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 fait que les threads n'utilisent pas pleinement les multicœurs. Donc: n'utilisez pas de threads avec python!
  2. Les objets que vous souhaitez renvoyer slow_loadde la base de données ne sont pas sélectionnables, ce qui signifie qu'ils ne peuvent pas simplement être passés entre les processus. Donc: pas de multitraitement avec des résultats de softwarecenter!
  3. La bibliothèque que vous appelez (softwarecenter.db) n'est pas threadsafe (semble inclure gtk ou similaire), donc appeler ces méthodes dans un thread entraîne un comportement étrange (dans mon test, tout, de 'ça marche' sur 'core dump' à simple quitter sans résultats). Donc: pas de threads avec softwarecenter.
  4. Chaque rappel asynchrone dans gtk ne devrait rien faire d' autre que de supprimer un rappel qui sera appelé dans la boucle principale de glib. Donc: non print, pas de changement d'état gtk, sauf l'ajout d'un rappel!
  5. Gtk et autres ne fonctionnent pas avec les threads hors de la boîte. Vous devez le faire threads_init, et si vous appelez une méthode gtk ou similaire, vous devez protéger cette méthode (dans les versions antérieures, c'était gtk.gdk.threads_enter(), gtk.gdk.threads_leave()voir par exemple gstreamer: http://pygstdocs.berlios.de/pygst-tutorial/playbin. html ).

Je peux vous faire la suggestion suivante:

  1. Réécrivez votre slow_loadpour retourner des résultats sélectionnables et utiliser des contrats à terme avec des processus.
  2. Passez de softwarecenter à python-apt ou similaire (vous n'aimez probablement pas ça). Mais depuis que vous êtes employé par Canonical, vous pouvez demander directement aux développeurs de softwarecenter d' ajouter de la documentation à leur logiciel (par exemple en déclarant qu'il n'est pas sûr pour les threads) et encore mieux, rendant softwarecenter threadsafe.

Comme une note: les solutions données par les autres ( Gio.io_scheduler_push_job, async_call) faire travailler avec , time.sleepmais pas avec softwarecenter.db. C'est parce que tout se résume à des threads ou des processus et des threads pour ne pas fonctionner avec gtk et softwarecenter.


Merci! Je vais accepter votre réponse car elle me montre très en détail pourquoi elle n'est pas faisable. Malheureusement, je ne peux pas utiliser de logiciel qui n'est pas packagé pour Ubuntu 12.04 dans mon application (c'est pour Quantal, bien que launchpad.net/ubuntu/+source/python-concurrent.futures ), donc je suppose que je suis coincé de ne pas pouvoir pour exécuter ma tâche de manière asynchrone. En ce qui concerne la note pour parler aux développeurs du Software Center, je suis dans la même position que tout volontaire pour apporter des modifications au code et à la documentation ou pour leur parler :-)
David Planella

GIL est libéré pendant IO, il est donc parfaitement possible d'utiliser des threads. Bien que cela ne soit pas nécessaire si async IO est utilisé.
jfs

10

Voici une autre option utilisant le planificateur d'E / S de GIO (je ne l'ai 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 __name__ == '__main__':
    main()

Voir aussi GIO.io_scheduler_job_send_to_mainloop (), si vous souhaitez exécuter quelque chose dans le thread principal une fois slow_stuff terminé.
Siegfried Gevatter

Merci Sigfried pour la réponse et l'exemple. Malheureusement, il semble qu'avec ma tâche actuelle, je n'ai aucune chance d'utiliser l'API Gio pour la faire fonctionner de manière asynchrone.
David Planella

C'était vraiment utile, mais pour autant que je sache, Gio.io_scheduler_job_send_to_mainloop n'existe pas en Python :(
sil

2

Vous pouvez également utiliser GLib.idle_add (rappel) pour appeler la tâche de longue durée une fois que le GLib Mainloop termine tous ses événements de priorité plus élevée (ce qui, je crois, inclut la construction de l'interface utilisateur).


Merci Mike. Oui, cela aiderait certainement à démarrer la tâche lorsque l'interface utilisateur est prête. Mais d'un autre côté, je comprends que lorsque callbackest appelé, cela se ferait de manière synchrone, bloquant ainsi l'interface utilisateur, non?
David Planella

L'idle_add ne fonctionne pas tout à fait comme ça. Faire des appels de blocage dans un idle_add est toujours une mauvaise chose à faire, et cela empêchera les mises à jour de l'interface utilisateur. Et même l'API asynchrone peut toujours être bloquante, où la seule façon d'éviter de bloquer l'interface utilisateur et d'autres tâches est de le faire dans un thread d'arrière-plan.
dobey

Idéalement, vous diviseriez votre tâche lente en morceaux, vous pouvez donc en exécuter un peu dans un rappel inactif, revenir (et laisser d'autres choses comme les rappels d'interface utilisateur s'exécuter), continuer à faire un peu plus de travail une fois votre rappel rappelé, et ainsi de suite sur.
Siegfried Gevatter

Un problème avec idle_addest que la valeur de retour du rappel est importante. Si c'est vrai, il sera rappelé.
Flimm

2

Utilisez l' GioAPI introspected pour lire un fichier, avec ses méthodes asynchrones, et lors de l'appel initial, faites-le comme un timeout avec GLib.timeout_add_seconds(3, call_the_gio_stuff)call_the_gio_stuffest une fonction qui retourne False.

Le délai d'attente ici est nécessaire pour ajouter (un nombre différent de secondes peut être requis, cependant), car bien que les appels asynchrones de Gio soient asynchrones, ils ne sont pas bloquants, ce qui signifie que l'activité lourde du disque de lecture d'un fichier volumineux ou volumineux nombre de fichiers, peut entraîner une interface utilisateur bloquée, car l'interface utilisateur et les E / S sont toujours dans le même thread (principal).

Si vous souhaitez écrire vos propres fonctions pour être asynchrones et les intégrer à la boucle principale, à l'aide des API d'E / S de fichiers de Python, vous devrez écrire le code en tant que GObject, ou pour passer des rappels, ou utiliser python-deferpour vous aider fais le. Mais il est préférable d'utiliser Gio ici, car il peut vous apporter de nombreuses fonctionnalités intéressantes, surtout si vous faites des choses d'ouverture / d'enregistrement de fichiers dans l'UX.


Merci @dobey. En fait, je ne lis pas directement un fichier à partir du disque, j'aurais probablement dû le clarifier dans le message d'origine. La tâche de longue haleine que j'exécute consiste à lire la base de données du centre logiciel conformément à la réponse à askubuntu.com/questions/139032/… , donc je ne suis pas sûr de pouvoir utiliser l' GioAPI. Ce que je me demandais, c'est s'il existe un moyen d'exécuter une tâche générique de longue durée de manière asynchrone, comme GTask le faisait auparavant.
David Planella

Je ne sais pas exactement ce qu'est GTask, mais si vous voulez dire gtask.sourceforge.net, je ne pense pas que vous devriez l'utiliser. Si c'est autre chose, je ne sais pas ce que c'est. Mais il semble que vous devrez emprunter la deuxième voie que j'ai mentionnée et implémenter une API asynchrone pour encapsuler ce code, ou tout simplement faire tout dans un thread.
dobey

Il y a un lien vers cela dans la question. GTask est (était): chergert.github.com/gtask
David Planella

1
Ah, cela ressemble beaucoup à l'API fournie par python-defer (et à l'API différée de twisted). Vous devriez peut-être envisager d'utiliser python-defer?
dobey

1
Vous devez toujours retarder l'appel, jusqu'à ce que les principaux événements prioritaires se soient produits, en utilisant GLib.idle_add () par exemple. Comme ceci: pastebin.ubuntu.com/1011660
dobey

1

Je pense qu'il convient de noter que c'est une façon compliquée de faire ce que @mhall a suggéré.

Essentiellement, vous avez un run this, puis exécutez cette fonction de async_call.

Si vous voulez voir comment cela fonctionne, vous pouvez jouer avec la minuterie de sommeil 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.

Sur cette base, ce 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 __name__ == '__main__':
    win = SlowLoad()
    win.show_all()
    #time.sleep(10)
    Gtk.main()

Remarque supplémentaire, vous devez laisser l'autre thread se terminer avant qu'il ne se termine correctement ou rechercher un fichier.lock dans votre thread enfant.

Modifier pour répondre au commentaire: au
départ, j'ai oublié GObject.threads_init(). Évidemment, lorsque le bouton a tiré, il a initialisé le filetage pour moi. Cela a masqué l'erreur pour moi.

Généralement, le flux consiste à créer la fenêtre en mémoire, à lancer immédiatement l'autre thread, lorsque le thread se termine, mettez à 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 s'exécuter avant même que la fenêtre ne soit dessinée. Je l'ai également commenté pour vérifier que le lancement du fil ne gêne pas du tout le dessin de la fenêtre.


1
Merci. Je ne suis pas sûr de pouvoir le suivre. D'une part, je m'attendais slow_loadà être exécuté peu de temps après le début de l'interface utilisateur, mais il ne semble jamais être appelé, à moins que le bouton ne soit cliqué, ce qui m'embrouille un peu, car je pensais que le but du bouton était simplement de fournir une indication visuelle de l'état de la tâche.
David Planella

Désolé, j'ai raté une ligne. Ça l'a fait. J'ai oublié de dire à GObject de se préparer pour les discussions.
RobotHumans

Mais vous appelez dans la boucle principale à partir d'un thread, ce qui peut causer des problèmes, bien qu'ils puissent ne pas être facilement exposés dans votre exemple trivial qui ne fait aucun travail réel.
dobey

Point valable, mais je ne pensais pas qu'un exemple trivial méritait d'envoyer la notification via DBus (ce que je pense qu'une application non triviale devrait faire)
RobotHumans

Hm, courir async_calldans cet exemple fonctionne pour moi, mais cela apporte le chaos quand je le porte à mon application et j'ajoute la vraie slow_loadfonction que j'ai.
David Planella
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.