Як запустити асинхронні завдання в програмах Introspection Python GObject


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

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

Чи є простий і широко застосовуваний спосіб виконання асинхронних завдань та отримання сповіщення про їх закінчення?


Це не дуже приклад, але я впевнений, що це те, що ви шукаєте: raw.github.com/gist/1132418/…
RobotHumans

Класно, я думаю, що твоя async_callфункція може бути те, що мені потрібно. Не хотіли б ви трохи розширити його і додати відповідь, щоб я міг прийняти його та зарахувати вас після тестування? Спасибі!
Девід Планелла

1
Чудове запитання, дуже корисне! ;-)
Rafał Cieślak

Відповіді:


15

Ваша проблема є дуже поширеною, тому є багато рішень (навіси, черги з багатопроцесорною чи нарізною роботою, робочі пули, ...)

Оскільки це так часто, існує також рішення для вбудовування python (в 3.2, але підтримується тут: http://pypi.python.org/pypi/futures ) під назвою concurrent.futures. "Ф'ючерси" доступні багатьма мовами, тому python називає їх однаковими. Ось типові дзвінки (і ось ваш повний приклад , проте частина 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, завдяки чому потоки не повністю використовують багатоядерні. Отже: не використовуйте теми з python!
  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. Перехід від програмного центру до python-apt або подібного (вам, мабуть, це не подобається). Але оскільки ви працюєте в Canonical, ви можете попросити розробників програмного центру безпосередньо додати документацію до свого програмного забезпечення (наприклад, заявивши, що це не безпечно для потоків), а ще краще, зробивши програмне забезпечення центром безпеки.

Як зауваження: рішення, надані іншими ( Gio.io_scheduler_push_job, async_call) , працюють, time.sleepале не з ними softwarecenter.db. Це тому, що все зводиться до ниток або процесів, і потоки не працюють з gtk і softwarecenter.


Спасибі! Я прийму вашу відповідь, оскільки вона дуже детально вказує на те, чому це неможливо зробити. На жаль, я не можу використовувати програмне забезпечення, яке не упаковані для Ubuntu 12.04 в моєму додатку (це для квантового, хоча launchpad.net/ubuntu/+source/python-concurrent.futures ), так що я припускаю , що я застряг з не в змозі запустити моє завдання асинхронно. Щодо записки про розмову з розробниками Програмного центру, я перебуваю на тій же позиції, що і будь-який доброволець, щоб внести зміни до коду та документації або поговорити з ними :-)
Девід Планелла,

GIL випускається під час IO, тому цілком чудово використовувати нитки. Хоча це не обов'язково, якщо використовується асинхронний IO.
jfs

10

Ось ще один варіант використання програми планування вводу / виводу 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.io_scheduler_job_send_to_mainloop (), якщо ви хочете запустити щось у головній темі після того, як slow_stuff закінчиться.
Зігфрід Геваттер

Дякую Сигфрід за відповідь та приклад. На жаль, здається, що в моєму поточному завданні я не маю шансів використовувати API Gio, щоб змусити його працювати асинхронно.
Девід Планелла

Це було дуже корисно, але наскільки я можу сказати, Gio.io_scheduler_job_send_to_mainloop не існує в Python :(
sil

2

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


Спасибі Майку. Так, це безумовно допоможе почати завдання, коли інтерфейс готовий. Але з іншого боку, я розумію, що коли callbackбуде викликано, це буде зроблено синхронно, тим самим блокуючи інтерфейс користувача, правда?
Девід Планелла

Idle_add не зовсім так працює. Блокувати виклики в idle_add - це все-таки погано, і це не дасть оновити інтерфейс користувача. І навіть асинхронний API все ще може блокувати, де єдиний спосіб уникнути блокування інтерфейсу користувача та інших завдань - це робити це у фоновому потоці.
добі

В ідеалі ви розділите свою повільну задачу на шматки, щоб ви могли запустити її трохи в режимі очікування, повернутись (і дозволити виконувати інші речі, такі як зворотні виклики інтерфейсу користувача), продовжувати виконувати ще одну роботу, як тільки ваш зворотний виклик буде викликаний знову на.
Зігфрід Геваттер

Одним словом idle_addє те, що значення зворотного дзвінка має значення. Якщо це правда, вона буде викликана знову.
Flimm

2

Використовуйте інтроспективний GioAPI для читання файлу з його асинхронними методами, а під час початкового виклику виконайте це як тайм-аут, GLib.timeout_add_seconds(3, call_the_gio_stuff)де call_the_gio_stuffфункція повертається False.

Час очікування тут потрібно додати (хоча може знадобитися різна кількість секунд), оскільки, хоча виклики Gio async асинхронні, вони не блокують, це означає, що активність великого диска при читанні великого файлу чи великого кількість файлів, це може призвести до заблокованого інтерфейсу, оскільки користувальницький інтерфейс та введення / виведення все ще знаходяться в одному (головному) потоці.

Якщо ви хочете написати свої власні функції для асинхронізації та інтегруватися з основним циклом, використовуючи файли вводу / виводу файлів Python, вам доведеться написати код у вигляді GObject, або передавати зворотні дзвінки навколо, або використовувати, python-deferщоб допомогти вам Зроби це. Але найкраще використовувати Gio тут, оскільки це може принести вам багато приємних функцій, особливо якщо ви робите відкриті / зберігання файлів в UX.


Дякую @dobey. Я насправді не читаю файл безпосередньо з диска, я, мабуть, зробив це ясніше в оригінальній публікації. Завдання, яке я виконую , - це читання бази даних Software Center відповідно до відповіді на askubuntu.com/questions/139032/… , тож я не впевнений, чи можу я використовувати GioAPI. Мені було цікаво, чи є спосіб запустити будь-яку загальну тривалу задачу асинхронно так само, як це робив GTask.
Девід Планелла

Я не знаю, що таке GTask, але якщо ви маєте на увазі gtask.sourceforge.net, то я не думаю, що вам слід це використовувати. Якщо це щось інше, то я не знаю, що це. Але це здається, що вам доведеться пройти другий маршрут, про який я згадав, і реалізувати якийсь асинхронний API, щоб обернути цей код, або просто зробити це все в потоці.
добі

У цьому питанні є посилання на нього. GTask є (був): chergert.github.com/gtask
Девід Планелла

1
Так, це дуже схоже на API, що надається python-defer (і відкладеним API відкладеного). Можливо, вам варто поглянути на використання python-defer?
добі

1
Вам потрібно затримати виклик, поки не відбудуться основні пріоритетні події, наприклад, використовуючи GLib.idle_add ().
Ось так

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

Додаткова примітка. Ви повинні дозволити іншому потоку закінчитись, перш ніж він належним чином закінчиться, або перевірити наявність файлу.блок у дочірній нитці.

Змінити на коментар до адреси:
Спочатку я забув GObject.threads_init(). Очевидно, коли кнопка спрацьовує, вона ініціалізувала набір різьби для мене. Це маскувало для мене помилку.

Зазвичай потоком є ​​створення вікна в пам'яті, негайно запустіть інший потік, коли потік завершить оновити кнопку. Я додав додатковий сон, перш ніж я навіть зателефонував на Gtk.main, щоб переконатися, що повне оновлення COULD запускається до того, як вікно було навіть намальоване. Я також прокоментував це, щоб переконатися, що запуск потоку зовсім не перешкоджає малюванню вікон.


1
Спасибі. Я не впевнений, що можу слідувати за цим. Для одного, я б очікував, що slow_loadвін буде виконаний незабаром після запуску інтерфейсу користувача, але він, здається, ніколи не викликається, якщо тільки не натиснути кнопку, що мене трохи бентежить, оскільки я вважав, що ціль цієї кнопки - просто надати візуальну індикацію стану завдання.
Девід Планелла

Вибачте, я пропустив один рядок. Це і вдалося. Я забув сказати GObject, щоб підготуватися до теми.
RobotHumans

Але ви зателефонуєте в основний цикл з потоку, що може спричинити проблеми, хоча вони можуть бути легко розкриті у вашому тривіальному прикладі, який не робить жодної реальної роботи.
добі

Дійсна точка, але я не думаю, що тривіальний приклад заслуговує на надсилання сповіщення через DBus (що, на мою думку, має робити нетривіальний додаток)
RobotHumans

Гм, запущений async_callу цьому прикладі працює для мене, але він приносить хаос, коли я переношу його у свою програму, і я додаю реальну slow_loadфункцію, яку я отримав.
Девід Планелла
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.