Come eseguire attività asincrone nelle app di Introspection di Python GObject

16

Sto scrivendo un'app Python + GObject che deve leggere una quantità non trascurabile di dati dal disco all'avvio. I dati vengono letti in modo sincrono e occorrono circa 10 secondi per terminare l'operazione di lettura, durante la 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 sono preoccupato che il suo codice non sia Sono stati toccati in 3 anni, e tanto meno sono stati portati su GObject Introspection. Ancora più importante, non è più disponibile in Ubuntu 12.04. Quindi sto cercando un modo semplice per eseguire attività in modo asincrono, sia in modo standard Python che in un modo standard GObject / GTK +.

Modifica: ecco un codice con un esempio di ciò che sto cercando di fare. Ho provato python-defer come suggerito nei commenti, ma non sono riuscito a eseguire l'attività lunga in modo asincrono e lasciare che l'interfaccia utente venga caricata senza dover attendere il termine. Sfoglia il codice di prova .

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

    
posta David Planella 27.05.2012 - 13:05

5 risposte

15

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

Poiché è così comune, esiste anche una soluzione di python built-in (in 3.2, ma backported qui: link ) chiamato 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 per il 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 Python hanno un GIL, il che rende i thread not completamente utilizzano i multicores. Quindi: non usare discussioni con python!
  2. Gli oggetti che si desidera restituire in slow_load dal DB non sono selezionabili, il che significa che non possono essere semplicemente passati tra i processi. Quindi: no multiprocessing con risultati softwarecenter!
  3. La libreria che chiami (softwarecenter.db) non è protetta da un thread (sembra includere gtk o simili), quindi chiamare questi metodi in un thread produce un comportamento strano (nel mio test, tutto da 'funziona' su 'core dump 'alla semplice smettere senza risultati). Quindi: niente thread con softwarecenter.
  4. Ogni callback asincrono in gtk non dovrebbe fare qualsiasi cosa tranne sheduling di un callback che verrà chiamato nel lolo mainloop di glib. Quindi: no print , nessuno stato gtk cambia, eccetto aggiungere un callback!
  5. Gtk e simili non funzionano con i thread fuori dalla scatola. Devi fare threads_init , e 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: link ).

Posso darti il seguente suggerimento:

  1. Riscrivi il slow_load per restituire i risultati selezionabili e utilizzare i future con i processi.
  2. Passa da softwarecenter a python-apt o simile (probabilmente non ti piace). Ma dal momento che sono impiegati da Canonical, puoi chiedere agli sviluppatori di software di softwarecenter di aggiungere documention al loro software (ad esempio dichiarando che non è thread-safe) e ancora meglio, rendendo softwarecenter threadsafe.

Come nota: le soluzioni date dagli altri ( Gio.io_scheduler_push_job , async_call ) do funzionano 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 .

    
risposta data xubuntix 28.05.2012 - 09:49
11

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

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()
    
risposta data Siegfried Gevatter 27.05.2012 - 18:24
2

Puoi anche usare GLib.idle_add (callback) per chiamare l'attività a lungo termine una volta che GLib Mainloop termina tutti gli eventi con priorità più alta (che credo includa la costruzione dell'interfaccia utente).

    
risposta data mhall119 27.05.2012 - 15:49
2

Utilizza l'API introspettiva Gio per leggere un file, con i suoi metodi asincroni, e quando effettui la chiamata iniziale, fallo come un timeout con GLib.timeout_add_seconds(3, call_the_gio_stuff) dove call_the_gio_stuff è una funzione che restituisce False .

Il timeout qui è necessario aggiungere (un numero diverso di secondi può essere richiesto, però), perché mentre le chiamate asincrone Gio sono asincrone, non sono non bloccanti, ovvero l'attività del disco pesante di leggere un file di grandi dimensioni , o un numero elevato 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 asincrono e integrarti con il loop principale, usando le API I / O dei file Python, dovrai scrivere il codice come GObject, o per passare i callback in giro o usare python-defer per aiutarti a farlo. Ma è meglio usare Gio qui, dato che può offrirti molte funzioni interessanti, specialmente se stai facendo aprire / salvare file nella UX.

    
risposta data dobey 27.05.2012 - 14:50
1

Penso che tenga presente che questo è un modo complesso di 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 lo sleep timer e continuare a fare clic sul pulsante. È essenzialmente uguale alla risposta di @ mhall eccetto che c'è 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 chiuda correttamente o controlla un file.lock nel tuo thread figlio.

Modifica per commentare l'indirizzo:
Inizialmente ho dimenticato GObject.threads_init() . Evidentemente quando il pulsante è stato attivato, 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 che chiamassi Gtk.main per verificare che l'aggiornamento completo potesse funzionare prima ancora che la finestra fosse disegnata. Ho anche commentato per verificare che il lancio del thread non impedisca affatto il disegno della finestra.

    
risposta data RobotHumans 27.05.2012 - 16:57

Leggi altre domande sui tag