Cómo ejecutar tareas asincrónicas en aplicaciones Python GObject Introspection

16

Estoy escribiendo una aplicación Python + GObject que necesita leer una cantidad no trivial de datos del disco al inicio. Los datos se leen sincrónicamente y se tarda unos 10 segundos en finalizar la operación de lectura, tiempo durante el cual la carga de la IU se retrasa.

Me gustaría ejecutar la tarea de forma asincrónica y recibir una notificación cuando esté lista, sin bloquear la interfaz de usuario, más o menos como:

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

He usado GTask en el pasado para este tipo de cosas, pero me preocupa que su código no haya sido tocado en 3 años, y mucho menos haya sido portado a GObject Introspection. Lo más importante, ya no está disponible en Ubuntu 12.04. Por lo tanto, estoy buscando una manera fácil de ejecutar tareas de forma asincrónica, ya sea en una forma estándar de Python o en una forma estándar GObject / GTK +.

Editar: aquí hay un código con un ejemplo de lo que estoy tratando de hacer. He intentado python-defercomo se sugiere en los comentarios, pero no pude ejecutar la tarea larga de forma asincrónica y dejar que la interfaz de usuario se cargue sin tener que esperar a que termine. Explore el código de prueba .

¿Existe una manera fácil y ampliamente utilizada de ejecutar tareas asincrónicas y recibir notificaciones cuando hayan terminado?

David Planella
fuente
No es un buen ejemplo, pero estoy bastante seguro de que esto es lo que está buscando: raw.github.com/gist/1132418/…
RobotHumans
Genial, creo que tu async_callfunción podría ser lo que necesito. ¿Te importaría expandirlo un poco y agregar una respuesta, para que pueda aceptarlo y acreditarte después de probarlo? ¡Gracias!
David Planella
1
Gran pregunta, muy útil! ;-)
Rafał Cieślak

Respuestas:

15

Su problema es muy común, por lo tanto, hay toneladas de soluciones (cobertizos, colas con multiprocesamiento o subprocesamiento, grupos de trabajadores, ...)

Como es tan común, también hay una solución incorporada de Python (en 3.2, pero con respaldo aquí: http://pypi.python.org/pypi/futures ) llamada concurrent.futures. Los 'futuros' están disponibles en muchos idiomas, por lo tanto, Python los llama de la misma manera. Aquí están las llamadas típicas (y aquí está su ejemplo completo , sin embargo, la parte db se reemplaza por dormir, vea a continuación por qué).

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)

Ahora a su problema, que es mucho más complicado de lo que sugiere su simple ejemplo. En general, tiene hilos o procesos para resolver esto, pero esta es la razón por la cual su ejemplo es tan complicado:

  1. La mayoría de las implementaciones de Python tienen un GIL, que crea subprocesos no utilicen completamente multinúcleos. Entonces: ¡no uses hilos con python!
  2. Los objetos que quieres devolver slow_load desde la base de datos no se pueden pickear, lo que significa que no se pueden pasar simplemente entre procesos. Entonces: ¡no multiprocesamiento con resultados de centro de software!
  3. La biblioteca que llama (softwarecenter.db) no es segura para hilos (parece incluir gtk o similar), por lo tanto, llamar a estos métodos en un hilo resulta en un comportamiento extraño (en mi prueba, todo, desde 'funciona' hasta 'volcado de núcleo' a simple dejar de fumar sin resultados). Entonces: no hay hilos con softwarecenter.
  4. Cada devolución de llamada asincrónica en gtk no debe hacer nada más que eliminar una devolución de llamada que se llamará en el bucle principal de glib. Entonces: noprint , no hay cambios de estado de gtk, ¡excepto agregar una devolución de llamada!
  5. Gtk y similares no funcionan con hilos fuera de la caja. Debe hacerlo threads_init, y si llama a un método gtk o similar, debe proteger ese método (en versiones anteriores esto era gtk.gdk.threads_enter(), gtk.gdk.threads_leave()ver, por ejemplo, gstreamer: http://pygstdocs.berlios.de/pygst-tutorial/playbin. html )

Puedo darte la siguiente sugerencia:

  1. Reescribe tu slow_load a escribir para devolver resultados de piquete y utilizar futuros con procesos.
  2. Cambie de softwarecenter a python-apt o similar (probablemente no le guste eso). Pero dado que Canonical lo empleó, puede pedirles a los desarrolladores del centro de software directamente que agreguen documentación a su software (por ejemplo, declarando que no es seguro para subprocesos) e incluso mejor, haciendo que el centro de software sea seguro.

Como nota: las soluciones dadas por los otros ( Gio.io_scheduler_push_job, async_call) hacer trabajo con time.sleeppero no con softwarecenter.db. Esto se debe a que todo se reduce a subprocesos o procesos y subprocesos para no funcionar con gtk y softwarecenter.

xubuntix
fuente
¡Gracias! Voy a aceptar su respuesta, ya que me señala con mucho detalle por qué no es factible. Desafortunadamente, no puedo usar un software que no esté empaquetado para Ubuntu 12.04 en mi aplicación (es para Quantal, aunque launchpad.net/ubuntu/+source/python-concurrent.futures ), así que supongo que estoy atascado con no poder para ejecutar mi tarea de forma asincrónica. Con respecto a la nota para hablar con los desarrolladores del Centro de software, estoy en la misma posición que cualquier voluntario para contribuir con cambios en el código y la documentación o para hablar con ellos :-)
David Planella
GIL se libera durante IO, por lo que está perfectamente bien usar hilos. Aunque no es necesario si se usa IO asíncrono.
jfs
10

Aquí hay otra opción que usa el Programador de E / S de GIO (nunca lo he usado desde Python, pero el ejemplo a continuación parece funcionar bien).

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
fuente
Vea también GIO.io_scheduler_job_send_to_mainloop (), si desea ejecutar algo en el hilo principal una vez que finalice slow_stuff.
Siegfried Gevatter
Gracias Sigfried por la respuesta y el ejemplo. Desafortunadamente, parece que con mi tarea actual no tengo oportunidad de usar la API de Gio para que se ejecute de forma asincrónica.
David Planella
Esto fue realmente útil, pero por lo que puedo decir, Gio.io_scheduler_job_send_to_mainloop no existe en Python :(
sil
2

También puede usar GLib.idle_add (devolución de llamada) para llamar a la tarea de ejecución larga una vez que GLib Mainloop finaliza todos sus eventos de mayor prioridad (que creo que incluye la construcción de la interfaz de usuario).

mhall119
fuente
Gracias Mike Sí, eso definitivamente ayudaría a comenzar la tarea cuando la IU esté lista. Pero, por otro lado, entiendo que cuando callbackse llama, eso se haría sincrónicamente, bloqueando así la interfaz de usuario, ¿verdad?
David Planella
Idle_add no funciona así. Hacer llamadas de bloqueo en un idle_add sigue siendo algo malo, y evitará que ocurran actualizaciones de la interfaz de usuario. E incluso la API asincrónica todavía puede estar bloqueando, donde la única forma de evitar el bloqueo de la interfaz de usuario y otras tareas, es hacerlo en un hilo de fondo.
dobey
Idealmente, dividiría su tarea lenta en trozos, para que pueda ejecutar un poco en una devolución de llamada inactiva, regresar (y dejar que se ejecuten otras cosas como devoluciones de llamada de UI), continuar trabajando un poco más una vez que se vuelva a llamar, y así en.
Siegfried Gevatter
Un problema con idle_addes que el valor de retorno de la devolución de llamada es importante. Si es cierto, se volverá a llamar.
Flimm
2

Use la GioAPI introspectada para leer un archivo, con sus métodos asincrónicos, y al hacer la llamada inicial, hágalo como un tiempo de espera con GLib.timeout_add_seconds(3, call_the_gio_stuff)donde call_the_gio_stuffhay una función que regresa False.

Es necesario agregar el tiempo de espera aquí (sin embargo, se puede requerir un número diferente de segundos), porque aunque las llamadas asincrónicas de Gio son asíncronas, no son sin bloqueo, lo que significa que la actividad de disco pesado de leer un archivo grande o grande número de archivos, puede provocar una IU bloqueada, ya que la IU y la E / S todavía están en el mismo hilo (principal).

Si desea escribir sus propias funciones para ser asíncrono e integrarse con el bucle principal, utilizando las API de E / S de archivos de Python, tendrá que escribir el código como un GObject, o pasar devoluciones de llamada, o usarlo python-deferpara ayudarlo hazlo. Pero es mejor usar Gio aquí, ya que puede brindarte muchas características agradables, especialmente si estás abriendo / guardando archivos en la UX.

dobey
fuente
Gracias @dobey. En realidad no estoy leyendo un archivo del disco directamente, probablemente debería haberlo aclarado en la publicación original. La tarea de larga duración que estoy ejecutando es leer la base de datos del Centro de software según la respuesta a askubuntu.com/questions/139032/… , por lo que no estoy seguro de poder usar la GioAPI. Lo que me preguntaba es si hay una manera de ejecutar cualquier tarea genérica de ejecución larga de forma asincrónica de la misma manera que solía hacer GTask.
David Planella
No sé qué es GTask exactamente, pero si te refieres a gtask.sourceforge.net, entonces no creo que debas usar eso. Si es otra cosa, entonces no sé qué es. Pero parece que tendrá que tomar la segunda ruta que mencioné e implementar alguna API asincrónica para envolver ese código, o simplemente hacerlo todo en un hilo.
dobey
Hay un enlace a esto en la pregunta. GTask es (era): chergert.github.com/gtask
David Planella
1
Ah, eso se parece mucho a la API proporcionada por python-defer (y la API diferida de twisted). ¿Quizás deberías considerar usar python-defer?
dobey
1
Todavía necesita retrasar la llamada, hasta después de que ocurran los eventos de prioridad principal, utilizando GLib.idle_add (), por ejemplo. Así: pastebin.ubuntu.com/1011660
dobey
1

Creo que vale la pena señalar que esta es una forma complicada de hacer lo que @mhall sugirió.

Esencialmente, tienes que ejecutar esto y luego ejecutar esa función de async_call.

Si quieres ver cómo funciona, puedes jugar con el temporizador de reposo y seguir haciendo clic en el botón. Es esencialmente lo mismo que la respuesta de @ mhall, excepto que hay un código de ejemplo.

Basado en esto, que no es mi trabajo.

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 adicional, debe dejar que el otro subproceso termine antes de que finalice correctamente o busque un file.lock en su subproceso secundario.

Editar al comentario de la dirección:
Inicialmente olvidéGObject.threads_init() . Evidentemente, cuando se activó el botón, se inició el enhebrado por mí. Esto enmascaró el error para mí.

En general, el flujo es crear la ventana en la memoria, iniciar inmediatamente el otro subproceso, cuando el subproceso completa la actualización del botón. Agregué una suspensión adicional antes de llamar a Gtk.main para verificar que la actualización completa PODRÍA ejecutarse antes de que la ventana se dibujara. También lo comenté para verificar que el inicio del hilo no impida en absoluto el dibujo de la ventana.

RobotHumanos
fuente
1
Gracias. No estoy seguro de poder seguirlo. Por un lado, hubiera esperado slow_loadque se ejecutara poco después de que se iniciara la interfaz de usuario, pero parece que nunca se llama, a menos que se haga clic en el botón, lo que me confunde un poco, ya que pensé que el propósito del botón era solo proporcionar una indicación visual. del estado de la tarea.
David Planella
Lo siento, me perdí una línea. Eso lo hizo. Olvidé decirle a GObject que se prepare para los hilos.
RobotHumans
Pero está llamando al bucle principal desde un hilo, lo que puede causar problemas, aunque es posible que no se expongan fácilmente en su ejemplo trivial que no hace ningún trabajo real.
dobey
Punto válido, pero no pensé que un ejemplo trivial mereciera enviar la notificación a través de DBus (que creo que debería estar haciendo una aplicación no trivial)
RobotHumans
Hm, ejecutar async_callen este ejemplo funciona para mí, pero trae caos cuando lo transfiero a mi aplicación y agrego la slow_loadfunción real que tengo.
David Planella