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

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

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

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 в прошлом для этого вида вещи, но я обеспокоен, что ее код не был затронут за 3 года, уже не говоря о, портированном к Самоанализу GObject. Самое главное это больше не доступно в Ubuntu 12.04. Таким образом, я ищу простой способ выполнить задачи асинхронно, или в стандартном Python путь или в GObject/GTK + стандартный путь.

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

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

16
задан 28 May 2012 в 03:13

5 ответов

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

0
ответ дан 28 May 2012 в 03:13

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

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

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

0
ответ дан 28 May 2012 в 03:13

Вот еще один вариант использования планировщика ввода-вывода 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()
0
ответ дан 28 May 2012 в 03:13

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

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

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, что делает потоки не полностью использующими многоядерные , Итак: не используйте потоки с python!
  2. Объекты, которые вы хотите вернуть в slow_load из БД, нельзя перехватывать, что означает, что их нельзя просто передавать между процессами. Итак: нет многопроцессорной обработки с результатами softwarecenter!
  3. Библиотека, которую вы вызываете (softwarecenter.db), не является потокобезопасной (кажется, включает gtk или аналогичную), поэтому вызов этих методов в потоке приводит к странному поведению (в моем тесте все: от «все работает» до «дампа ядра» - до простого выхода без результатов). Итак: нет потоков с программным центром.
  4. Каждый асинхронный обратный вызов в gtk не должен делать ничего , кроме планирования обратного вызова, который будет вызываться в главном цикле glib. Итак: нет print, нет изменений состояния gtk, кроме добавления обратного вызова!
  5. Gtk и другие не работают с потоками из коробки. Вам нужно сделать threads_init, и если вы вызываете метод gtk или аналогичный, вы должны защитить этот метод (в более ранних версиях это было gtk.gdk.threads_enter(), gtk.gdk.threads_leave(). См., Например, gstreamer: http: // pygstdocs.berlios.de/pygst-tutorial/playbin.html).

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

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

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

0
ответ дан 28 May 2012 в 03:13

Думаю, стоит отметить, что это запутанный способ сделать то, что предложил @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()

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

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

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

0
ответ дан 28 May 2012 в 03:13

Другие вопросы по тегам:

Похожие вопросы: