Как запускать асинхронные задачи в приложениях 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 как предложено в комментариях, но мне не удалось выполнить длинную задачу асинхронно и позволить загрузке пользовательского интерфейса, не дожидаясь ее завершения. Просмотрите код теста.
Существует ли простой и широко используемый способ запуска асинхронных задач и получения уведомлений по их окончании?
5 ответов
Ваша проблема является очень распространенной, поэтому существует множество решений (сараи, очереди с многопроцессорной обработкой или многопоточностью, рабочие пулы, ...)
Поскольку это так часто встречается, существует также встроенное решение для 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)
Теперь к вашей проблеме, которая намного сложнее, чем предлагает ваш простой пример. В общем, у вас есть потоки или процессы, чтобы решить эту проблему, но вот почему ваш пример такой сложный:
- Большинство реализаций Python имеют GIL, что делает потоки не полностью использующими многоядерные. Итак: не используйте темы с питоном!
- Объекты, которые вы хотите вернуть в
slow_loadиз БД не могут быть извлечены, что означает, что они не могут просто передаваться между процессами. Итак: нет многопроцессорной обработки с результатами программного центра! - Библиотека, которую вы вызываете (softwarecenter.db), не является потокобезопасной (кажется, включает gtk или аналогичную), поэтому вызов этих методов в потоке приводит к странному поведению (в моем тесте все, от "все работает", до "дампа ядра", до простого выход без результатов). Итак: нет темы с софтцентром.
- Каждый асинхронный обратный вызов в gtk не должен делать ничего, кроме планирования обратного вызова, который будет вызываться в главном цикле glib. Итак: нет
print, нет изменений состояния GTK, кроме добавления обратного вызова! - Gtk и другие не работают с потоками из коробки. Вам нужно сделать
threads_initи если вы вызываете метод gtk или аналогичный, вы должны защитить этот метод (в более ранних версиях это былоgtk.gdk.threads_enter(),gtk.gdk.threads_leave(), см., например, gstreamer: http://pygstdocs.berlios.de/pygst-tutorial/playbin.html).
Я могу дать вам следующее предложение:
- Перепишите свой
slow_loadвозвращать результаты поиска и использовать фьючерсы с процессами. - Переключитесь с softwarecenter на python-apt или аналогичный (вам, вероятно, это не нравится). Но поскольку вы работаете в Canonical, вы можете напрямую попросить разработчиков программного центра добавить документацию к своему программному обеспечению (например, заявив, что она не является поточно-ориентированной) и, что еще лучше, сделать программный центр безопасным для потоков.
Как примечание: решения, предоставленные другими (Gio.io_scheduler_push_job, async_call) работать с time.sleep но не с softwarecenter.db, Это потому, что все сводится к потокам или процессам и потокам, чтобы не работать с gtk и softwarecenter,
Вот еще один вариант использования планировщика ввода-вывода 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()
Используйте самоанализ Gio API для чтения файла с его асинхронными методами и при первоначальном вызове делает это как тайм-аут с GLib.timeout_add_seconds(3, call_the_gio_stuff) где call_the_gio_stuff это функция, которая возвращает False,
Здесь необходимо добавить время ожидания (хотя может потребоваться иное количество секунд), поскольку асинхронные вызовы Gio асинхронны, но не неблокированы, а это означает, что при чтении большого файла или большого объема данных на жестком диске Количество файлов, может привести к блокировке пользовательского интерфейса, так как пользовательский интерфейс и ввод-вывод все еще находятся в одном (основном) потоке.
Если вы хотите написать свои собственные функции, которые должны быть асинхронными, и интегрироваться с основным циклом, используя API-интерфейсы файлового ввода-вывода Python, вам придется написать код как GObject или передать обратные вызовы или использовать python-defer чтобы помочь вам сделать это. Но лучше использовать Gio здесь, так как он может принести вам много полезных функций, особенно если вы делаете открытия / сохранения файлов в UX.
Вы также можете использовать GLib.idle_add(callback), чтобы вызвать долгосрочную задачу, когда GLib Mainloop завершит все события с более высоким приоритетом (что, я считаю, включает в себя создание пользовательского интерфейса).
Я думаю, стоит отметить, что это запутанный способ сделать то, что предложил @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, чтобы убедиться, что полное обновление МОЖЕТ выполняться до того, как окно будет нарисовано. Я также прокомментировал это, чтобы убедиться, что запуск потока вообще не мешает рисованию окна.