Como executar tarefas assíncronas nos aplicativos Python GObject Introspection

16

Estou escrevendo um aplicativo Python + GObject que precisa ler uma quantidade não trivial de dados do disco no início. Os dados são lidos de forma síncrona e leva cerca de 10 segundos para concluir a operação de leitura, período durante o qual o carregamento da interface do usuário é atrasado.

Gostaria de executar a tarefa de forma assíncrona e receber uma notificação quando estiver pronta, sem bloquear a interface do usuário, mais ou 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()

Eu usei o GTask no passado para esse tipo de coisa, mas estou preocupado que seu código não seja tocado em três anos, muito menos tenha sido portado para o GObject Introspection. Mais importante, ele não está mais disponível no Ubuntu 12.04. Portanto, estou procurando uma maneira fácil de executar tarefas de forma assíncrona, tanto no modo Python padrão quanto no GObject / GTK +.

Edit: aqui está um código com um exemplo do que estou tentando fazer. Tentei python-deferconforme sugerido nos comentários, mas não consegui executar a tarefa longa de forma assíncrona e deixar a interface do usuário carregar sem ter que esperar a conclusão. Procure o código de teste .

Existe uma maneira fácil e amplamente usada de executar tarefas assíncronas e ser notificado quando elas terminarem?

David Planella
fonte
Não é um exemplo muito, mas eu tenho certeza que isso é o que você está procurando: raw.github.com/gist/1132418/...
RobotHumans
Legal, acho que sua async_callfunção pode ser o que eu preciso. Você se importaria de expandir um pouco e adicionar uma resposta, para que eu possa aceitá-la e lhe creditar depois de testá-la? Obrigado!
David Planella 27/05
11
Ótima pergunta, muito útil! ;-)
Rafał Cieślak

Respostas:

15

Seu problema é muito comum, portanto, existem inúmeras soluções (galpões, filas com multiprocessamento ou encadeamento, pools de trabalhadores, ...)

Por ser tão comum, também existe uma solução embutida em python (na 3.2, mas com suporte aqui: http://pypi.python.org/pypi/futures ) chamada concurrent.futures. 'Futuros' estão disponíveis em vários idiomas; portanto, python os chama da mesma forma. Aqui estão as chamadas típicas (e aqui está o seu exemplo completo , no entanto, a parte db é substituída por sleep, veja abaixo o porquê).

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)

Agora, para o seu problema, que é muito mais complicado do que o seu exemplo simples sugere. Em geral, você tem threads ou processos para resolver isso, mas aqui está o porquê de seu exemplo ser tão complicado:

  1. A maioria das implementações do Python possui um GIL, que faz com que os threads não utilizem totalmente multicores. Então: não use threads com python!
  2. Os objetos que você deseja retornar slow_loaddo banco de dados não são selecionáveis, o que significa que eles não podem simplesmente ser passados ​​entre processos. Portanto: não há multiprocessamento com resultados do softwarecenter!
  3. A biblioteca que você chama (softwarecenter.db) não é thread-safe (parece incluir gtk ou similar), portanto, chamar esses métodos em um thread resulta em um comportamento estranho (no meu teste, tudo, desde 'funciona' sobre 'core dump' até simples sair sem resultados). Portanto: sem threads com o softwarecenter.
  4. Todo retorno de chamada assíncrono no gtk não deve fazer nada, exceto eliminar um retorno de chamada que será chamado no mainloop glib. Portanto: não print, nenhum estado do gtk muda, exceto a adição de um retorno de chamada!
  5. Gtk e similares não funcionam com threads fora da caixa. Você precisa fazer isso threads_inite, se você chamar um método gtk ou semelhante, precisará protegê-lo (nas versões anteriores, isso eragtk.gdk.threads_enter() , gtk.gdk.threads_leave()veja. Por exemplo, gstreamer: http://pygstdocs.berlios.de/pygst-tutorial/playbin. html ).

Eu posso lhe dar a seguinte sugestão:

  1. Reescreva seu slow_loadpara retornar resultados selecionáveis ​​e usar futuros com processos.
  2. Mude do softwarecenter para python-apt ou similar (você provavelmente não gosta disso). Mas como você trabalha na Canonical, você pode pedir diretamente aos desenvolvedores do softwarecenter para adicionar documentação ao software (por exemplo, declarar que não é seguro para threads) e, melhor ainda, tornar o softwarecenter seguro.

Como uma nota: as soluções dadas pelos outros ( Gio.io_scheduler_push_job, async_call) fazer o trabalho com time.sleepmas não com softwarecenter.db. Isto é, porque tudo se resume a threads ou processos e threads para não funcionar com o gtk e softwarecenter.

xubuntix
fonte
Obrigado! Vou aceitar sua resposta, pois ela me mostra muito detalhadamente o motivo de não ser possível. Infelizmente, não posso usar software que não esteja empacotado para o Ubuntu 12.04 no meu aplicativo (é para Quantal, embora launchpad.net/ubuntu/+source/python-concurrent.futures ), então acho que estou preso em não poder para executar minha tarefa de forma assíncrona. Em relação à nota para falar com os desenvolvedores de Software Centre, estou na mesma situação que qualquer voluntário para contribuir alterações no código e documentação ou para falar com eles :-)
David Planella
O GIL é lançado durante o IO, portanto, é perfeitamente adequado usar threads. Embora não seja necessário se E / S assíncrona for usada.
jfs
10

Aqui está outra opção usando o GIO I / O Scheduler (nunca o usei antes no Python, mas o exemplo abaixo parece funcionar bem).

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
fonte
Consulte também GIO.io_scheduler_job_send_to_mainloop (), se você deseja executar algo no thread principal quando o slow_stuff terminar.
Siegfried Gevatter
Obrigado Sigfried pela resposta e pelo exemplo. Infelizmente, parece que com a minha tarefa atual não tenho chance de usar a API Gio para executá-la de forma assíncrona.
David Planella 28/05
Isso foi realmente útil, mas, tanto quanto eu posso dizer, Gio.io_scheduler_job_send_to_mainloop não existe no Python :(
sil
2

Você também pode usar o GLib.idle_add (retorno de chamada) para chamar a tarefa de longa execução quando o Mainloop do GLib concluir todos os seus eventos de maior prioridade (que eu acredito que inclui a construção da interface do usuário).

mhall119
fonte
Obrigado Mike. Sim, isso definitivamente ajudaria a iniciar a tarefa quando a interface do usuário estiver pronta. Mas, por outro lado, entendo que quando callbacké chamado, isso seria feito de forma síncrona, bloqueando a interface do usuário, certo?
David Planella 27/05
O idle_add não funciona assim. Fazer chamadas de bloqueio em um idle_add ainda é uma coisa ruim a ser feita e impedirá que ocorram atualizações na interface do usuário. E mesmo a API assíncrona ainda pode estar bloqueando, onde a única maneira de evitar o bloqueio da interface do usuário e outras tarefas, é fazê-lo em um encadeamento em segundo plano.
Dobey
Idealmente, você dividiria sua tarefa lenta em partes, para que você possa executá-la em um retorno de chamada inativo, retornar (e permitir que outras coisas como retornos de chamada da interface do usuário sejam executadas), continuar fazendo mais trabalho depois que o retorno de chamada for chamado novamente, e assim em.
Siegfried Gevatter
Um problema idle_addé que o valor de retorno do retorno de chamada é importante. Se for verdade, será chamado novamente.
Flimm
2

Use a GioAPI introspectada para ler um arquivo, com seus métodos assíncronos, e ao fazer a chamada inicial, faça isso como um tempo limite com GLib.timeout_add_seconds(3, call_the_gio_stuff)where call_the_gio_stuffé uma função que retornaFalse .

O tempo limite aqui é necessário adicionar (embora seja necessário um número diferente de segundos), porque, embora as chamadas Gio assíncronas sejam assíncronas, elas não são bloqueadoras, o que significa que a atividade pesada do disco de ler um arquivo grande ou grande número de arquivos, pode resultar na interface do usuário bloqueada, pois a interface do usuário e a E / S ainda estão no mesmo encadeamento (principal).

Se você deseja escrever suas próprias funções para ser assíncrono e integrar-se ao loop principal, usando as APIs de E / S de arquivos do Python, precisará escrever o código como um GObject, ou repassar retornos de chamada ou usar python-deferpara ajudá-lo. faça. Mas é melhor usar o Gio aqui, pois ele pode oferecer muitos recursos interessantes, principalmente se você estiver abrindo / salvando arquivos no UX.

dobey
fonte
Obrigado @dobey. Na verdade, não estou lendo diretamente um arquivo do disco, provavelmente deveria ter deixado isso mais claro na postagem original. A tarefa de execução demorada que estou executando é ler o banco de dados do Software Center conforme a resposta a askubuntu.com/questions/139032/… , portanto, não tenho certeza se posso usar a GioAPI. O que eu queria saber é se existe uma maneira de executar qualquer tarefa genérica de longa duração de forma assíncrona, da mesma forma que o GTask costumava fazer.
David Planella 27/05
Não sei exatamente o que é o GTask, mas se você quer dizer gtask.sourceforge.net , não acho que deva usá-lo. Se é outra coisa, então eu não sei o que é. Mas parece que você terá que seguir a segunda rota que mencionei e implementar alguma API assíncrona para quebrar esse código, ou simplesmente fazer tudo em um encadeamento.
Dobey
Há um link para isso na pergunta. O GTask é (foi): chergert.github.com/gtask
David Planella
11
Ah, isso parece muito com a API fornecida pelo python-defer (e pela API adiada do twisted). Talvez você deva usar python-defer?
Dobey
11
Você ainda precisa atrasar a chamada, até que os principais eventos de prioridade ocorram, usando GLib.idle_add () por exemplo. Como assim: pastebin.ubuntu.com/1011660
dobey
1

Acho que vale a pena notar que essa é uma maneira complicada de fazer o que o @mhall sugeriu.

Essencialmente, você pode executar isso e executar a função async_call.

Se você quiser ver como ele funciona, você pode jogar com o temporizador e continuar clicando no botão. É essencialmente o mesmo que a resposta de @ mhall, exceto que há um código de exemplo.

Com base nisso, esse não é o meu trabalho.

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

Observação adicional: você deve deixar o outro encadeamento terminar antes que ele termine corretamente ou procurar um arquivo.lock no encadeamento filho.

Editar para endereçar o comentário:
Inicialmente esqueci GObject.threads_init(). Evidentemente, quando o botão disparou, ele inicializou o threading para mim. Isso mascarou o erro para mim.

Geralmente, o fluxo é criar a janela na memória, iniciar imediatamente o outro encadeamento, quando o encadeamento terminar atualizar o botão. Adicionei um sono adicional antes mesmo de ligar para Gtk.main para verificar se a atualização completa PODE ser executada antes mesmo da janela ser desenhada. Também comentei isso para verificar se o lançamento do thread não impede o desenho da janela.

RobotHumans
fonte
11
Obrigado. Não tenho certeza se posso segui-lo. Por um lado, eu esperava slow_loadser executado logo após o início da interface do usuário, mas nunca parece ser chamado, a menos que o botão seja clicado, o que me confunde um pouco, pois pensei que o objetivo do botão era apenas fornecer indicação visual do estado da tarefa.
David Planella
Desculpe, eu perdi uma linha. Isso foi o que aconteceu. Esqueci de dizer ao GObject para se preparar para os tópicos.
RobotHumans
Mas você está chamando o loop principal a partir de um thread, o que pode causar problemas, embora eles não sejam facilmente expostos no seu exemplo trivial, que não faz nenhum trabalho real.
Dobey
Ponto válido, mas eu não acho que um exemplo trivial mereceu o envio da notificação via DBus (que eu acho que um aplicativo não-trivial deveria estar fazendo)
RobotHumans
Hum, rodar async_callneste exemplo funciona para mim, mas gera confusão quando eu o porto para o meu aplicativo e adiciono a slow_loadfunção real que tenho.
David Planella 27/05