Как запускать асинхронные задачи в приложениях Proson GObject Introspection

16

Я пишу приложение Python + GObject, которому нужно прочитать нетривиальное количество данных с диска при запуске. Данные считываются синхронно, и для завершения операции чтения требуется около 10 секунд, в течение которых время задержки пользовательского интерфейса задерживается.

Я хотел бы запустить задачу асинхронно и получить уведомление, когда оно будет готово, без блокировки пользовательского интерфейса, более или менее:

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

Я использовал GTask в прошлом для такого рода вещей, но я обеспокоен тем, что его код hasn ' t были затронуты через 3 года, не говоря уже о портировании в GOZDECT Introspection. Самое главное, что он больше не доступен в Ubuntu 12.04. Поэтому я ищу простой способ запускать задачи асинхронно, либо стандартным способом Python, либо стандартным способом GObject / GTK +.

Изменить: вот какой-то код с примером того, что я пытаюсь сделать. Я пробовал python-defer , как было предложено в комментариях, но я не мог выполнить асинхронную длительную задачу и позволить загружать пользовательский интерфейс без необходимости ждать его завершения. Просмотрите тестовый код .

Есть ли простой и широко используемый способ запуска асинхронных задач и получать уведомления, когда они закончены?

    
задан David Planella 27.05.2012 в 13:05
источник

5 ответов

15

Ваша проблема очень распространенная, поэтому есть множество решений (навесы, очереди с многопроцессорной или поточной обработкой, пулы работников, ...)

Так как это так часто, существует также встроенное решение python (в 3.2, но приведено здесь: Ссылка ) называется concurrent.futures. «Фьючерсы» доступны на многих языках, поэтому питон называет их одинаковыми. Вот типичные вызовы (и вот ваш полный пример , однако часть db заменяется на сон, см. Ниже почему) ,

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)

Теперь к вашей проблеме, которая намного сложнее, чем предлагает ваш простой пример. В общем, у вас есть потоки или процессы для решения этой проблемы, но вот почему ваш пример настолько сложный:

  1. В большинстве реализаций Python есть GIL, который делает потоки не полностью использовать multicores. Итак: не используйте потоки с python!
  2. Объекты, которые вы хотите вернуть в slow_load из БД, не подбираются, что означает, что они не могут просто передаваться между процессами. Итак: нет многопроцессорности с результатами программного обеспечения!
  3. Библиотека, которую вы вызываете (softwarecenter.db), не является потокобезопасной (кажется, включает в себя gtk или аналогичную), поэтому вызов этих методов в потоке приводит к странному поведению (в моем тесте все, начиная с «он работает» над «дампом ядра» 'к простому отказу без результатов). Итак: нет потоков с программным центром.
  4. Каждый асинхронный обратный вызов в gtk не должен выполнять что-либо , за исключением того, что будет выполняться обратный вызов, который будет вызываться в glib mainloop. Итак: no print , никаких изменений состояния gtk, кроме добавления обратного вызова!
  5. Gtk и, похоже, не работают с потоками из коробки. Вам нужно сделать threads_init , и если вы вызываете метод gtk или подобный, вы должны защитить этот метод (в более ранних версиях это было gtk.gdk.threads_enter() , gtk.gdk.threads_leave() . См. Например gstreamer: Ссылка ).

Я могу дать вам следующее предложение:

  1. Перепишите свой slow_load , чтобы вернуть полученные результаты и использовать фьючерсы с процессами.
  2. Переключение с softwarecenter на python-apt или аналогичное (вам, вероятно, это не нравится). Но так как вы наняли Canonical, вы можете попросить разработчиков программного обеспечения непосредственно добавить документацию к своему программному обеспечению (например, заявив, что он не является потокобезопасным) и даже лучше, что делает программный центр потокобезопасным.

В качестве примечания: решения, предоставленные другими ( Gio.io_scheduler_push_job , async_call ) do , работают с time.sleep , но не с softwarecenter.db . Это потому, что все это сводится к потокам, процессам и потокам, которые не работают с gtk и softwarecenter .

    
ответ дан xubuntix 28.05.2012 в 09:49
источник
11

Вот еще один вариант с использованием планировщика ввода-вывода GIO (я никогда не использовал его раньше из Python, но пример ниже, похоже, работает нормально).

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 27.05.2012 в 18:24
2

Вы также можете использовать GLib.idle_add (обратный вызов) для вызова долговременной задачи после того, как GLib Mainloop завершит все события с более высоким приоритетом (что, я считаю, включает в себя создание пользовательского интерфейса).

    
ответ дан mhall119 27.05.2012 в 15:49
2

Использовать проверенный Gio API для чтения файла с его асинхронными методами и при выполнении начального вызова сделать это как тайм-аут с GLib.timeout_add_seconds(3, call_the_gio_stuff) , где call_the_gio_stuff - это функция, которая возвращает False .

Тайм-аут здесь необходимо добавить (может потребоваться другое количество секунд), потому что, в то время как асинхронные вызовы Gio являются асинхронными, они не являются неблокирующими, что означает, что активность жесткого диска при чтении большого файла , или большое количество файлов, может привести к блокировке пользовательского интерфейса, поскольку пользовательский интерфейс и ввод-вывод все еще находятся в одном и том же (основном) потоке.

Если вы хотите написать свои собственные функции для async и интегрироваться с основным циклом, используя API-интерфейсы ввода-вывода Python, вам придется писать код как GObject или передавать обратные вызовы или использовать python-defer , чтобы помочь вам это сделать. Но лучше всего использовать Gio здесь, так как это может принести вам массу приятных функций, особенно если вы делаете файл открытым / сохранить материал в UX.

    
ответ дан dobey 27.05.2012 в 14:50
1

Я думаю, что он отмечает, что это сложный способ сделать то, что предложил @mhall.

По существу, у вас есть запуск, а затем запустите эту функцию async_call.

Если вы хотите посмотреть, как это работает, вы можете играть с таймером сна и продолжать нажимать кнопку. Это по сути то же самое, что и ответ @ mhall, за исключением того, что есть пример кода.

Исходя из этого , который не является моей работой.

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

Кроме того, вы должны разрешить другому потоку до того, как он завершится правильно, или проверьте файл file.lock в дочернем потоке.

Изменить комментарий к адресу:
Первоначально я забыл GObject.threads_init() . Очевидно, когда кнопка срабатывала, для меня была инициализирована резьба. Это замаскировало ошибку для меня.

Обычно поток создает окно в памяти, сразу же запускает другой поток, когда поток завершает обновление кнопки. Я добавил дополнительный сон, прежде чем я даже позвонил в Gtk.main, чтобы убедиться, что полное обновление COULD запустилось до того, как окно было даже рисовано. Я также прокомментировал это, чтобы убедиться, что запуск потока не мешает рисованию окна.

    
ответ дан RobotHumans 27.05.2012 в 16:57