it-roy-ru.com

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

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

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

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

16
David Planella

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

Поскольку это так часто встречается, есть также встроенное решение python (в 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, что делает потоки не полностью использующими многоядерные. Итак: не используйте темы с питоном!
  2. Объекты, которые вы хотите вернуть в slow_load из БД, нельзя перехватывать, что означает, что их нельзя просто передавать между процессами. Итак: нет многопроцессорной обработки с результатами программного центра!
  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. Переключитесь с softwarecenter на python-apt или аналогичный (вам, вероятно, это не нравится). Но поскольку вы работаете в Canonical, вы можете попросить разработчиков центра программного обеспечения напрямую добавить документацию в их программное обеспечение (например, заявив, что оно не является поточно-ориентированным), и даже лучше, делая программный центр потокобезопасным.

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

15
xubuntix

Вот еще один вариант использования планировщика ввода-вывода 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 __== '__main__':
    main()
11
Siegfried Gevatter

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

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

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

2
dobey

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

2
mhall119

Я думаю, стоит отметить, что это запутанный способ сделать то, что предложил @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 __== '__main__':
    win = SlowLoad()
    win.show_all()
    #time.sleep(10)
    Gtk.main()

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

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

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

1
RobotHumans