Come eseguire attività asincrone nelle app Python GObject Introspection


16

Sto scrivendo un'app Python + GObject che deve leggere una quantità non banale di dati dal disco all'avvio. I dati vengono letti in modo sincrono e sono necessari circa 10 secondi per completare l'operazione di lettura, durante il quale il caricamento dell'interfaccia utente viene ritardato.

Vorrei eseguire l'attività in modo asincrono e ricevere una notifica quando è pronta, senza bloccare l'interfaccia utente, più o meno come:

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

Ho usato GTask in passato per questo genere di cose, ma temo che il suo codice non sia stato toccato da 3 anni, per non parlare del porting su GObject Introspection. Ancora più importante, non è più disponibile in Ubuntu 12.04. Quindi sto cercando un modo semplice per eseguire le attività in modo asincrono, in modo Python standard o in modo GObject / GTK + standard.

Modifica: ecco un po 'di codice con un esempio di ciò che sto cercando di fare. Ho provato python-defercome suggerito nei commenti, ma non sono riuscito a eseguire il task lungo in modo asincrono e lasciare caricare l'interfaccia utente senza dover attendere che finisca. Sfoglia il codice di prova .

Esiste un modo semplice e ampiamente utilizzato per eseguire attività asincrone e ricevere notifiche al termine?


Non è un bell'esempio, ma sono abbastanza sicuro che questo è quello che stai cercando: raw.github.com/gist/1132418/…
RobotHumans

Bene, penso che la tua async_callfunzione potrebbe essere quella di cui ho bisogno. Ti dispiacerebbe ampliarlo un po 'e aggiungere una risposta, in modo che io possa accettarlo e accreditarti dopo averlo testato? Grazie!
David Planella,

1
Ottima domanda, molto utile! ;-)
Rafał Cieślak

Risposte:


15

Il tuo problema è molto comune, quindi ci sono tonnellate di soluzioni (capannoni, code con multiprocessing o threading, pool di lavoratori, ...)

Dato che è così comune, esiste anche una soluzione build-in di Python (in 3.2, ma backportata qui: http://pypi.python.org/pypi/futures ) chiamata concurrent.futures. I "futures" sono disponibili in molte lingue, quindi python li chiama allo stesso modo. Ecco le chiamate tipiche (ed ecco il tuo esempio completo , tuttavia, la parte db è sostituita da sleep, vedi sotto perché).

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)

Ora al tuo problema, che è molto più complicato di quanto suggerisce il tuo semplice esempio. In generale hai thread o processi per risolvere questo problema, ma ecco perché il tuo esempio è così complicato:

  1. La maggior parte delle implementazioni di Python ha un GIL, che rende i thread che non utilizzano completamente i multicore. Quindi: non usare thread con Python!
  2. Gli oggetti che si desidera restituire slow_loaddal DB non sono selezionabili, il che significa che non possono semplicemente essere passati tra i processi. Quindi: nessuna elaborazione multipla con risultati di softwarecenter!
  3. La libreria che chiami (softwarecenter.db) non è threadsafe (sembra includere gtk o simili), quindi chiamare questi metodi in un thread provoca uno strano comportamento (nel mio test, tutto da "funziona" su "core dump" a semplice smettere senza risultati). Quindi: nessun thread con softwarecenter.
  4. Ogni callback asincrono in gtk non dovrebbe fare altro che eliminare un callback che verrà chiamato nel glilo mainloop. Quindi: no print, non cambia lo stato gtk, tranne l'aggiunta di un callback!
  5. Gtk e simili non funzionano con i thread fuori dalla scatola. Devi farlo threads_inite se chiami un metodo gtk o simile, devi proteggere quel metodo (nelle versioni precedenti questo era gtk.gdk.threads_enter(), gtk.gdk.threads_leave()vedi ad esempio gstreamer: http://pygstdocs.berlios.de/pygst-tutorial/playbin. html ).

Posso darti il ​​seguente suggerimento:

  1. Riscrivi il tuo slow_loadper restituire risultati selezionabili e utilizzare i future con i processi.
  2. Passa da softwarecenter a python-apt o simili (probabilmente non ti piace). Ma dal momento che sei stato assunto da Canonical, potresti chiedere agli sviluppatori di softwarecenter di aggiungere direttamente la documentazione al loro software (ad esempio affermando che non è sicuro per i thread) e, ancora meglio, rendere sicuro il thread di softwarecenter.

Come nota: le soluzioni presentate dagli altri ( Gio.io_scheduler_push_job, async_call) fare il lavoro con time.sleep, ma non con softwarecenter.db. Questo perché tutto si riduce a thread o processi e thread per non funzionare con gtk e softwarecenter.


Grazie! Accetterò la tua risposta perché mi indica in modo molto dettagliato perché non è fattibile. Sfortunatamente, non posso usare software che non è impacchettato per Ubuntu 12.04 nella mia app (è per Quantal, anche se launchpad.net/ubuntu/+source/python-concurrent.futures ), quindi credo di essere bloccato per non essere in grado per eseguire il mio compito in modo asincrono. Per quanto riguarda la nota per parlare con gli sviluppatori del Software Center, sono nella stessa posizione di qualsiasi volontario per apportare modifiche al codice e alla documentazione o per parlare con loro :-)
David Planella,

GIL viene rilasciato durante l'IO, quindi è perfettamente corretto usare i thread. Sebbene non sia necessario se si utilizza un IO asincrono.
jfs,

10

Ecco un'altra opzione che utilizza l'I / O Scheduler di GIO (non l'ho mai usato prima da Python, ma l'esempio che segue sembra funzionare correttamente).

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

Vedi anche GIO.io_scheduler_job_send_to_mainloop (), se vuoi eseguire qualcosa nel thread principale al termine di slow_stuff.
Siegfried Gevatter

Grazie Sigfried per la risposta e l'esempio. Sfortunatamente, sembra che con il mio compito attuale non ho alcuna possibilità di usare l'API Gio per farla funzionare in modo asincrono.
David Planella,

Questo è stato davvero utile, ma per quanto ne so Gio.io_scheduler_job_send_to_mainloop non esiste in Python :(
sil

2

Puoi anche utilizzare GLib.idle_add (callback) per chiamare l'attività di lunga durata una volta che GLib Mainloop termina tutti gli eventi con priorità più alta (che credo includono la creazione dell'interfaccia utente).


Grazie Mike. Sì, ciò aiuterebbe sicuramente l'avvio dell'attività quando l'interfaccia utente è pronta. D'altra parte, capisco che quando callbackviene chiamato, ciò viene fatto in modo sincrono, bloccando così l'interfaccia utente, giusto?
David Planella,

Idle_add non funziona così. Effettuare il blocco delle chiamate in idle_add è ancora una cosa negativa da fare e impedirà che si verifichino aggiornamenti all'interfaccia utente. E anche l'API asincrona può ancora essere bloccata, dove l'unico modo per evitare di bloccare l'interfaccia utente e altre attività è farlo in un thread in background.
Dobey,

Idealmente, dovresti dividere l'attività lenta in blocchi, in modo da poter eseguire un po 'di esso in un callback inattivo, tornare (e lasciare che altre cose come i callback dell'interfaccia utente vengano eseguite), continuare a fare un po' più di lavoro una volta chiamato il callback, e così su.
Siegfried Gevatter

Un gotcha con idle_addè che il valore di ritorno del callback è importante. Se è vero, verrà chiamato di nuovo.
Flimm,

2

Utilizzare l' GioAPI introspetta per leggere un file, con i suoi metodi asincroni, e quando si effettua la chiamata iniziale, farlo come timeout con GLib.timeout_add_seconds(3, call_the_gio_stuff)dove call_the_gio_stuffè una funzione che ritorna False.

È necessario aggiungere il timeout qui (potrebbe essere necessario un numero diverso di secondi, tuttavia), poiché mentre le chiamate asincrone Gio sono asincrone, non sono non bloccanti, il che significa che l'attività pesante del disco nella lettura di un file di grandi dimensioni o grande numero di file, può causare un'interfaccia utente bloccata, poiché l'interfaccia utente e l'I / O sono ancora nello stesso thread (principale).

Se vuoi scrivere le tue funzioni in modo che siano asincrone e si integrino con il ciclo principale, usando le API di I / O del file Python, dovrai scrivere il codice come GObject, o passare i callback in giro, o usare python-deferper aiutarti fallo. Ma è meglio usare Gio qui, in quanto può offrirti molte funzioni interessanti, specialmente se stai facendo file apri / salva cose nella UX.


Grazie @dobey. In realtà non sto leggendo direttamente un file dal disco, probabilmente avrei dovuto renderlo più chiaro nel post originale. L'attività di lunga durata che sto eseguendo sta leggendo il database di Software Center secondo la risposta a askubuntu.com/questions/139032/… , quindi non sono sicuro di poter usare l' GioAPI. Quello che mi chiedevo è se esiste un modo per eseguire in modo asincrono qualsiasi attività generica di lunga durata nello stesso modo in cui faceva GTask.
David Planella,

Non so cosa sia esattamente GTask, ma se intendi gtask.sourceforge.net , non penso che dovresti usarlo. Se è qualcos'altro, allora non so cosa sia. Ma sembra che dovrai prendere la seconda strada che ho menzionato e implementare alcune API asincrone per avvolgere quel codice o semplicemente fare tutto in un thread.
Dobey,

C'è un link ad esso nella domanda. GTask è (era): chergert.github.com/gtask
David Planella,

1
Ah, sembra molto simile all'API fornita da python-defer (e dall'API differita di twisted). Forse dovresti guardare usando Python-Defer?
Dobey,

1
È ancora necessario ritardare la chiamata, fino a quando non si sono verificati i principali eventi prioritari, ad esempio utilizzando GLib.idle_add (). In questo modo: pastebin.ubuntu.com/1011660
dobey,

1

Penso che valga la pena notare che questo è un modo contorto per fare ciò che @mhall ha suggerito.

Essenzialmente, hai una corsa, quindi esegui quella funzione di async_call.

Se vuoi vedere come funziona, puoi giocare con il timer di spegnimento e continuare a fare clic sul pulsante. È essenzialmente lo stesso della risposta di @ mhall, tranne per il fatto che esiste un codice di esempio.

Basato su questo che non è il mio lavoro.

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

Nota aggiuntiva, devi lasciare che l'altro thread finisca prima che si concluda correttamente o controlli un file.lock nel tuo thread figlio.

Modifica per rispondere al commento:
inizialmente ho dimenticato GObject.threads_init(). Evidentemente quando il pulsante ha sparato, ha inizializzato il threading per me. Questo ha mascherato l'errore per me.

Generalmente il flusso è creare la finestra in memoria, avviare immediatamente l'altro thread, quando il thread completo aggiorna il pulsante. Ho aggiunto un ulteriore sonno prima ancora di chiamare Gtk.main per verificare che l'aggiornamento completo POTREBBE essere eseguito prima ancora che la finestra fosse disegnata. L'ho anche commentato per verificare che l'avvio del thread non impedisca affatto il disegno della finestra.


1
Grazie. Non sono sicuro di poterlo seguire. Per uno, mi sarei aspettato slow_loaddi essere eseguito poco dopo l'avvio dell'interfaccia utente, ma non sembra mai essere chiamato, a meno che non si faccia clic sul pulsante, il che mi confonde un po ', poiché pensavo che lo scopo del pulsante fosse solo quello di fornire un'indicazione visiva dello stato dell'attività.
David Planella,

Scusa, ho perso una riga. Quello l'ha fatto. Ho dimenticato di dire a GObject di essere pronto per le discussioni.
RobotHumans,

Ma stai chiamando nel loop principale da un thread, il che può causare problemi, anche se potrebbero non essere facilmente esposti nel tuo banale esempio che non fa alcun lavoro reale.
Dobey,

Punto valido, ma non pensavo che un esempio banale meritasse l'invio della notifica tramite DBus (che penso dovrebbe fare un'app non banale)
RobotHumans,

Hm, l'esecuzione async_callin questo esempio funziona per me, ma crea confusione quando lo porto sulla mia app e aggiungo la vera slow_loadfunzione che ho.
David Planella,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.