Ausführen asynchroner Aufgaben in Python GObject Introspection-Apps

16

Ich schreibe eine Python + GObject-App, die beim Start eine nicht unbedeutende Datenmenge von der Festplatte lesen muss. Die Daten werden synchron gelesen und es dauert ungefähr 10 Sekunden, bis der Lesevorgang abgeschlossen ist. Während dieser Zeit verzögert sich das Laden der Benutzeroberfläche.

Ich möchte die Aufgabe asynchron ausführen und eine Benachrichtigung erhalten, wenn sie fertig ist, ohne die Benutzeroberfläche zu blockieren.

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

In der Vergangenheit habe ich GTask für diese Art von Dingen verwendet, aber ich bin besorgt, dass der Code seit drei Jahren nicht mehr geändert wurde , geschweige denn nach GObject Introspection portiert wurde. Vor allem ist es in Ubuntu 12.04 nicht mehr verfügbar. Daher suche ich nach einer einfachen Möglichkeit, Aufgaben asynchron auszuführen, entweder auf Standard-Python-Weise oder auf GObject / GTK + -Standard-Weise.

Bearbeiten: Hier ist ein Code mit einem Beispiel, was ich versuche zu tun. Ich habe versucht, python-deferwie in den Kommentaren vorgeschlagen, aber ich konnte es nicht schaffen, die lange Aufgabe asynchron auszuführen und die Benutzeroberfläche laden zu lassen, ohne auf den Abschluss warten zu müssen. Durchsuchen Sie den Testcode .

Gibt es eine einfache und weit verbreitete Möglichkeit, asynchrone Aufgaben auszuführen und benachrichtigt zu werden, wenn sie abgeschlossen sind?

David Planella
quelle
Es ist kein schönes Beispiel, aber ich bin mir ziemlich sicher, dass Sie genau das
RobotHumans
Cool, ich denke deine async_callFunktion könnte das sein, was ich brauche. Würde es Ihnen etwas ausmachen, es etwas zu erweitern und eine Antwort hinzuzufügen, damit ich es akzeptieren und Ihnen gutschreiben kann, nachdem ich es getestet habe? Vielen Dank!
David Planella
1
Tolle Frage, sehr nützlich! ;-)
Rafał Cieślak

Antworten:

15

Ihr Problem ist sehr verbreitet, daher gibt es unzählige Lösungen (Schuppen, Warteschlangen mit Mehrfachverarbeitung oder Threading, Worker-Pools, ...)

Da dies so häufig vorkommt, gibt es auch eine integrierte Python-Lösung (in Version 3.2, hier jedoch als Backport: http://pypi.python.org/pypi/futures ) mit dem Namen concurrent.futures. Futures sind in vielen Sprachen verfügbar, daher nennt Python sie auch so. Hier sind die typischen Anrufe (und hier ist Ihr vollständiges Beispiel , jedoch wird der DB-Teil durch Schlaf ersetzt, siehe unten, warum).

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)

Nun zu Ihrem Problem, das viel komplizierter ist, als Ihr einfaches Beispiel nahelegt. Im Allgemeinen haben Sie Threads oder Prozesse, um dies zu lösen, aber hier ist der Grund, warum Ihr Beispiel so kompliziert ist:

  1. Die meisten Python-Implementierungen haben eine GIL, wodurch Threads Multicores nicht voll ausnutzen. Also: keine Threads mit Python verwenden!
  2. Die Objekte, die Sie slow_loadaus der Datenbank zurückgeben möchten, sind nicht auswählbar. Dies bedeutet, dass sie nicht einfach zwischen Prozessen übergeben werden können. Also: keine Mehrfachverarbeitung mit Softwarecenter-Ergebnissen!
  3. Die von Ihnen aufgerufene Bibliothek (softwarecenter.db) ist nicht threadsicher (scheint gtk oder ähnliches zu enthalten). Daher führt das Aufrufen dieser Methoden in einem Thread zu einem merkwürdigen Verhalten (in meinem Test reicht alles von "Funktioniert" über "Kernspeicherauszug" bis zu "Einfach") ergebnislos beenden). Also: keine Threads mit Softwarecenter.
  4. Jeder asynchrone Callback in gtk sollte nichts anderes tun , als einen Callback einzuplanen, der im glib-mainloop aufgerufen wird. Also: nein print, keine Änderungen des GTK-Status, außer das Hinzufügen eines Rückrufs!
  5. Gtk und ähnlich funktioniert nicht mit Threads aus der Box. Sie tun müssen threads_init, und wenn Sie ein gtk oder gleich Methode aufrufen, müssen Sie diese Methode schützen (in früheren Versionen war gtk.gdk.threads_enter(), gtk.gdk.threads_leave()zum Beispiel gstreamer sehen. Http://pygstdocs.berlios.de/pygst-tutorial/playbin. html ).

Ich kann Ihnen folgenden Vorschlag machen:

  1. slow_loadSchreiben Sie Ihre Daten neu, um aussagekräftige Ergebnisse zu erhalten und Futures für Prozesse zu verwenden.
  2. Wechseln Sie von Softwarecenter zu Python-Apt oder ähnlichem (das gefällt Ihnen wahrscheinlich nicht). Da Sie jedoch bei Canonical angestellt sind, können Sie die Softwarecenter-Entwickler direkt auffordern, ihrer Software eine Dokumentation hinzuzufügen (z. B. die Aussage, dass sie nicht threadsicher ist), und dies noch besser, um das Softwarecenter threadsicher zu machen.

Als Anmerkung: die von den anderen gegeben Lösungen ( Gio.io_scheduler_push_job, async_call) zu tun Arbeit mit , time.sleepaber nicht mit softwarecenter.db. Dies liegt daran, dass alles auf Threads oder Prozesse und Threads hinausläuft, um nicht mit gtk und zu funktionieren softwarecenter.

xubuntix
quelle
Vielen Dank! Ich werde Ihre Antwort akzeptieren, da sie mich sehr genau darauf hinweist, warum dies nicht machbar ist. Leider kann ich in meiner App keine Software verwenden, die nicht für Ubuntu 12.04 gepackt ist (dies gilt für Quantal, obwohl launchpad.net/ubuntu/+source/python-concurrent.futures ) meine Aufgabe asynchron ausführen. In Bezug auf die Notiz, mit den Software-Center-Entwicklern zu sprechen, bin ich in der gleichen Position wie jeder Freiwillige, der Änderungen am Code und an der Dokumentation beisteuert oder mit ihnen spricht :-)
David Planella
GIL wird während der E / A-Operation freigegeben, sodass die Verwendung von Threads völlig in Ordnung ist. Bei Verwendung von asynchronem E / A ist dies jedoch nicht erforderlich.
jfs
10

Hier ist eine weitere Option, die den I / O-Scheduler von GIO verwendet (ich habe ihn noch nie in Python verwendet, aber das folgende Beispiel scheint einwandfrei zu funktionieren).

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()
Siegfried Gevatter
quelle
Siehe auch GIO.io_scheduler_job_send_to_mainloop (), wenn Sie etwas im Hauptthread ausführen möchten, sobald slow_stuff beendet ist.
Siegfried Gevatter
Danke Sigfried für die Antwort und das Beispiel. Leider scheint es, dass ich bei meiner aktuellen Aufgabe keine Chance habe, die Gio-API zu verwenden, um sie asynchron laufen zu lassen.
David Planella
Das war wirklich nützlich, aber soweit ich das beurteilen kann, existiert Gio.io_scheduler_job_send_to_mainloop in Python nicht :(
sil
2

Sie können auch GLib.idle_add (Rückruf) verwenden, um die Task mit langer Laufzeit aufzurufen, sobald der GLib-Mainloop alle Ereignisse mit höherer Priorität abgeschlossen hat (was meines Erachtens das Erstellen der Benutzeroberfläche einschließt).

mhall119
quelle
Danke Mike. Ja, das würde definitiv beim Starten der Aufgabe helfen, wenn die Benutzeroberfläche fertig ist. Aber auf der anderen Seite verstehe ich, dass wenn callbackaufgerufen wird, dies synchron erfolgen würde, wodurch die Benutzeroberfläche blockiert würde, oder?
David Planella
Das idle_add funktioniert so nicht ganz. Das Blockieren von Aufrufen in idle_add ist immer noch eine schlechte Sache und verhindert, dass Aktualisierungen der Benutzeroberfläche durchgeführt werden. Und selbst asynchrone APIs können blockiert werden. Die einzige Möglichkeit, die Blockierung der Benutzeroberfläche und anderer Aufgaben zu vermeiden, besteht darin, sie in einem Hintergrundthread auszuführen.
Dobey
Idealerweise würden Sie Ihre langsame Aufgabe in Blöcke aufteilen, damit Sie einen Teil davon in einem inaktiven Rückruf ausführen, zurückkehren (und andere Dinge wie UI-Rückrufe ausführen lassen) und weiterarbeiten können, sobald Ihr Rückruf erneut aufgerufen wird auf.
Siegfried Gevatter
Ein Problem idle_addist, dass der Rückgabewert des Rückrufs von Bedeutung ist. Wenn es wahr ist, wird es erneut aufgerufen.
Flimm
2

Verwenden Sie die introspected- GioAPI, um eine Datei mit ihren asynchronen Methoden zu lesen, und tun Sie dies beim ersten Aufruf als Zeitüberschreitung, GLib.timeout_add_seconds(3, call_the_gio_stuff)wenn call_the_gio_stuffeine Funktion zurückgegeben wird False.

Das Zeitlimit muss hier hinzugefügt werden (es kann jedoch eine andere Anzahl von Sekunden erforderlich sein), da die asynchronen Gio-Aufrufe zwar asynchron sind, aber nicht blockieren Die Anzahl der Dateien kann zu einer Blockierung der Benutzeroberfläche führen, da sich Benutzeroberfläche und E / A immer noch im selben (Haupt-) Thread befinden.

Wenn Sie Ihre eigenen Funktionen so schreiben möchten, dass sie asynchron sind und mit Pythons Datei-E / A-APIs in die Hauptschleife integriert werden, müssen Sie den Code als GObject schreiben oder Rückrufe weitergeben oder python-deferals Hilfe verwenden Tu es. Aber es ist am besten, Gio hier zu verwenden, da es Ihnen viele nette Funktionen bieten kann, besonders wenn Sie Dateien in der UX öffnen / speichern.

dobey
quelle
Vielen Dank @dobey. Ich lese eine Datei nicht direkt von der Festplatte, das hätte ich wahrscheinlich im ursprünglichen Beitrag klarer machen sollen. Die langjährige Aufgabe, die ich ausführe , ist das Lesen der Software Center-Datenbank gemäß der Antwort auf askubuntu.com/questions/139032/…. Daher bin ich nicht sicher, ob ich die GioAPI verwenden kann. Was ich mich gefragt habe, ist, ob es eine Möglichkeit gibt, eine generische Aufgabe mit langer Laufzeit asynchron auszuführen, wie GTask es früher getan hat.
David Planella
Ich weiß nicht genau, was GTask ist, aber wenn Sie gtask.sourceforge.net meinen, dann denke ich nicht, dass Sie das verwenden sollten. Wenn es etwas anderes ist, dann weiß ich nicht, was es ist. Aber es hört sich so an, als müssten Sie den zweiten von mir erwähnten Weg einschlagen und eine asynchrone API implementieren, um diesen Code zu umbrechen, oder einfach alles in einem Thread.
Dobey
In der Frage gibt es einen Link dazu. GTask ist (war): chergert.github.com/gtask
David Planella
1
Ah, das sieht der API von python-defer (und der verzögerten API von twisted) sehr ähnlich. Vielleicht solltest du dir die Verwendung von Python-Defer ansehen?
Dienstag,
1
Sie müssen den Aufruf immer noch verzögern, bis die Ereignisse mit der Hauptpriorität eingetreten sind, indem Sie beispielsweise GLib.idle_add () verwenden. Wie folgt aus : pastebin.ubuntu.com/1011660
Dobey
1

Ich denke, es ist erwähnenswert, dass dies eine verschlungene Methode ist, um das zu tun, was @mhall vorgeschlagen hat.

Im Wesentlichen müssen Sie dies ausführen und dann die Funktion async_call ausführen.

Wenn Sie sehen möchten, wie es funktioniert, können Sie mit dem Sleep-Timer spielen und auf die Schaltfläche klicken. Es ist im Wesentlichen dasselbe wie die Antwort von @ mhall, außer dass es Beispielcode gibt.

Darauf basierend ist das nicht meine Arbeit.

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

Zusätzlicher Hinweis, Sie müssen den anderen Thread beenden lassen, bevor er ordnungsgemäß beendet wird, oder nach einer Dateisperre in Ihrem untergeordneten Thread suchen.

An Adresse ändern Kommentar:
Anfangs habe ich vergessen GObject.threads_init(). Offensichtlich wurde das Threading für mich initialisiert, als der Knopf ausgelöst wurde. Dies maskierte den Fehler für mich.

Im Allgemeinen wird das Fenster im Speicher erstellt und der andere Thread sofort gestartet, wenn der Thread die Aktualisierung der Schaltfläche abgeschlossen hat. Ich habe einen zusätzlichen Ruhezustand hinzugefügt, bevor ich Gtk.main angerufen habe, um zu überprüfen, ob das vollständige Update ausgeführt werden KANN, bevor das Fenster überhaupt gezeichnet wurde. Ich habe es auch auskommentiert, um sicherzustellen, dass der Thread-Start das Zeichnen von Fenstern überhaupt nicht behindert.

RobotHumans
quelle
1
Vielen Dank. Ich bin nicht sicher, ob ich dem folgen kann. Zum einen hätte ich erwartet slow_load, dass es kurz nach dem Start der Benutzeroberfläche ausgeführt wird, aber es scheint nie aufgerufen zu werden, es sei denn, die Schaltfläche ist angeklickt, was mich ein wenig verwirrt, da ich dachte, dass der Zweck der Schaltfläche nur die visuelle Anzeige ist des Zustands der Aufgabe.
David Planella
Entschuldigung, ich habe eine Zeile verpasst. Das hat es geschafft. Ich habe vergessen, GObject anzuweisen, sich für Threads vorzubereiten.
RobotHumans
Aber Sie rufen von einem Thread aus in die Hauptschleife auf, was zu Problemen führen kann, obwohl diese in Ihrem trivialen Beispiel, das keine wirkliche Arbeit leistet, möglicherweise nicht einfach offengelegt werden.
Dobey
Gültiger Punkt, aber ich dachte nicht, dass ein einfaches Beispiel es verdient, die Benachrichtigung über DBus zu senden (was meiner Meinung nach eine nicht
einfache
Hm, das Ausführen async_callin diesem Beispiel funktioniert für mich, aber es bringt Chaos, wenn ich es auf meine App portiere und die eigentliche slow_loadFunktion hinzufüge, die ich habe.
David Planella