Como executar tarefas assíncronas em aplicativos do 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 GTask no passado para esse tipo de coisa, mas estou preocupado que o código não tenha Foi tocado em 3 anos, muito menos foi portado para o GObject Introspection. Mais importante ainda, não está mais disponível no Ubuntu 12.04. Então, estou procurando uma maneira fácil de executar tarefas de forma assíncrona, seja de uma maneira padrão do Python ou de uma maneira padrão do GObject / GTK +.

Edit: aqui está um código com um exemplo do que estou tentando fazer. Eu tentei python-defer como 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 que ela fosse concluída. Navegue pelo código de teste .

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

    
por David Planella 27.05.2012 / 13:05

5 respostas

15

Seu problema é muito comum, por isso existem toneladas de soluções (galpões, filas com multiprocessamento ou encadeamento, pools de trabalho, etc.)

Como é tão comum, há também uma solução integrada em Python (na versão 3.2, mas backported aqui: link ) chamado concorrente.futures. 'Futuros' estão disponíveis em muitos 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 por que) .

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 eis porque seu exemplo é tão complicado:

  1. A maioria das implementações em Python tem um GIL, o que faz com que os threads não utilizem totalmente os multicores. Então: não use threads com python!
  2. Os objetos que você deseja retornar em slow_load do banco de dados não são pickelable, o que significa que eles não podem simplesmente ser transmitidos entre processos. Então: não há multiprocessamento com resultados softwarecenter!
  3. A biblioteca que você chama (softwarecenter.db) não é threadsafe (parece incluir gtk ou similar), portanto, chamar esses métodos em um encadeamento resulta em um comportamento estranho (no meu teste, tudo desde 'funciona' por 'core dump' 'para simplesmente desistir sem resultados). Então: sem threads com softwarecenter.
  4. Todo retorno de chamada assíncrono no gtk não deve fazer qualquer coisa , exceto o sheduling de um retorno de chamada, que será chamado no mainloop simplificado. Então: não print , nenhum estado gtk muda, exceto adicionar um retorno de chamada!
  5. Gtk e similares não funcionam com threads prontos para uso. Você precisa fazer threads_init , e se você chamar um método gtk ou similar, você tem que proteger esse método (em versões anteriores era gtk.gdk.threads_enter() , gtk.gdk.threads_leave() . Veja por exemplo gstreamer: link ).

Eu posso lhe dar a seguinte sugestão:

  1. Reescreva seu slow_load para retornar resultados pickelable e use futuros com processos.
  2. Mude de softwarecenter para python-apt ou similar (provavelmente você não gosta disso). Mas, como você é funcionário da Canonical, pode pedir diretamente aos desenvolvedores do softwarecenter para adicionar documentação ao software (por exemplo, declarar que não é thread safe) e, melhor ainda, tornar o softwarecenter thread-safe.

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

    
por xubuntix 28.05.2012 / 09:49
11

Aqui está outra opção usando o Agendador de E / S do GIO (eu nunca usei antes do Python, mas o exemplo abaixo parece rodar 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()
    
por Siegfried Gevatter 27.05.2012 / 18:24
2

Você também pode usar o GLib.idle_add (callback) para chamar a tarefa de execução longa, uma vez que o GLib Mainloop conclua todos os seus eventos de maior prioridade (o que, acredito, inclui a construção da UI).

    
por mhall119 27.05.2012 / 15:49
2

Use a API Gio introspectiva 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) , em que call_the_gio_stuff é uma função que retorna False . / p>

O tempo limite aqui é necessário para adicionar (um número diferente de segundos pode ser necessário, embora), porque enquanto as chamadas assíncronas do Gio são assíncronas, elas não são não-bloqueantes, significando 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, já que a interface do usuário e a E / S ainda estão no mesmo thread (principal).

Se você quiser escrever suas próprias funções para ser assíncrono e integrar com o loop principal, usando as APIs de E / S de arquivos do Python, você terá que escrever o código como um GObject, ou passar retornos de chamada, ou usar python-defer para ajudar você a fazer isso. Mas é melhor usar o Gio aqui, pois ele pode trazer muitos recursos interessantes, especialmente se você estiver fazendo arquivos abertos / salvos no UX.

    
por dobey 27.05.2012 / 14:50
1

Eu acho que é importante notar que esta é uma maneira complicada de fazer o que o @mhall sugeriu.

Essencialmente, você tem uma corrida e então executa essa função de async_call.

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

Baseado nisso que nã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()

Nota adicional, você tem que deixar o outro segmento terminar antes de finalizar corretamente ou procurar por um arquivo. bloquear no seu segmento filho.

Editar para comentar o comentário:
Inicialmente esqueci GObject.threads_init() . Evidentemente, quando o botão disparou, inicializou a segmentação para mim. Isso mascarou o erro por mim.

Geralmente o fluxo é criar a janela na memória, iniciar imediatamente o outro segmento, quando o segmento conclui atualizar o botão. Eu adicionei um sono adicional antes mesmo de ligar para o Gtk.main para verificar se a atualização completa PODERIA ser executada antes mesmo que a janela fosse desenhada. Eu também comentei para verificar se o lançamento do thread não impede o desenho da janela.

    
por RobotHumans 27.05.2012 / 16:57