如何在Python GObject Introspection应用程序中运行异步任务


16

我正在编写一个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 Introspection了。最重要的是,它在Ubuntu 12.04中不再可用。因此,我正在寻找一种以标准Python方式或GObject / GTK +标准方式异步运行任务的简便方法。

编辑:这是一些代码,其中包含我正在尝试执行的示例。我已经python-defer按照注释中的建议进行了尝试,但是我无法异步运行长任务并让UI加载而不必等待它完成。浏览测试代码

是否有一种简单且广泛使用的方式来运行异步任务并在完成任务时得到通知?


这不是一个糟糕的榜样,但我敢肯定这是你在找什么:raw.github.com/gist/1132418/...
RobotHumans

太好了,我认为您的async_call功能可能就是我所需要的。您介意对其进行扩展并添加答案,以便我接受它并在测试后将其归功于您吗?谢谢!
戴维·普拉内拉

1
好问题,非常有用!;-)
拉法尔Cieślak

Answers:


15

您的问题很常见,因此有很多解决方案(棚,具有多处理或线程的队列,工作池等)

由于它是如此普遍,因此还有一个称为并发的python内置解决方案(在3.2中,但已移植到此处:http : //pypi.python.org/pypi/futures)。'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要从数据库返回的对象是不可拾取的,这意味着不能简单地在进程之间传递它们。因此:不会在软件中心结果中进行多处理!
  3. 您调用的库(softwarecenter.db)不是线程安全的(似乎包含gtk或类似的代码),因此在线程中调用这些方法会导致奇怪的行为(在我的测试中,从“工作”到“核心转储”的一切都变得简单)退出而没有结果)。因此:软件中心没有线程。
  4. gtk中的每个异步回调都不应做任何事情,除了保留将在glib mainloop中调用的回调。所以:不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_jobasync_call与工作time.sleep,但不是softwarecenter.db。这是因为,所有原因都归结为线程或进程以及线程无法与gtk和一起使用softwarecenter


谢谢!我将接受您的回答,因为它非常详细地说明了为什么它不可行。不幸的是,我无法在我的应用程序中使用未为Ubuntu 12.04打包的软件(它是针对Quantal的,尽管launchpad.net/ubuntu/+source/python-concurrent.futures),所以我想我无法使用异步运行我的任务。关于与软件中心开发人员进行交流的说明,我与任何自愿对代码和文档进行更改或与他们进行交流的志愿者处于同一职位:-)
David Planella 2012年

GIL是在IO期间发布的,因此使用线程非常好。尽管使用异步IO并不是必须的。
jfs 2012年

10

这是使用GIO的I / O Scheduler的另一个选项(我以前从未在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()

如果您要在slow_stuff完成后在主线程中运行某些内容,请参见GIO.io_scheduler_job_send_to_mainloop()。
齐格菲·吉夫特

感谢Sigfried的回答和示例。不幸的是,似乎在我当前的任务中,我没有机会使用Gio API使其异步运行。
大卫·普拉内拉

这确实很有用,但据我所知,Gio.io_scheduler_job_send_to_mainloop在Python中不存在:(
sil

2

一旦GLib Mainloop完成了所有更高优先级的事件(我相信包括构建UI),您也可以使用GLib.idle_add(callback)调用长时间运行的任务。


谢谢迈克。是的,这肯定会在UI就绪时启动任务。但另一方面,我了解到,当callback调用时,它将同步完成,从而阻塞了UI,对吗?
戴维·普拉内拉

idle_add不太像那样。在idle_add中进行阻止调用仍然是一件不好的事,这将阻止对UI进行更新。甚至异步API仍然可以阻塞,避免阻塞UI和其他任务的唯一方法是在后台线程中执行。

理想情况下,您将缓慢的任务拆分为多个块,因此您可以在空闲的回调中运行其中的一部分,返回(并让UI回调之类的其他东西运行),一旦再次调用回调,继续做更多的工作,等等上。
齐格弗里德·格瓦特

需要idle_add注意的是,回调的返回值很重要。如果为true,将再次调用它。
Flimm

2

使用自省GioAPI读取文件及其异步方法,并在进行初始调用时将其作为超时,并使用GLib.timeout_add_seconds(3, call_the_gio_stuff)where call_the_gio_stuff函数返回return False

这里的超时是必须添加的(尽管可能需要不同的秒数),因为尽管Gio异步调用是异步的,但它们不是非阻塞的,这意味着读取大文件或读取大文件的繁重磁盘活动文件数量过多,可能会导致UI阻塞,因为UI和I / O仍位于同一(主)线程中。

如果要编写自己的异步函数,并使用Python的文件I / O API与主循环集成,则必须将代码编写为GObject,或传递回调,或使用它python-defer来帮助您做吧。但是最好在这里使用Gio,因为它可以为您带来很多不错的功能,尤其是当您在UX中执行文件打开/保存操作时。


谢谢@dobey。我实际上并不是直接从磁盘读取文件,我应该在原始文章中更清楚地指出。根据askubuntu.com/questions/139032/…的答案,我正在运行的长期任务是读取Software Center数据库,因此我不确定是否可以使用该GioAPI。我想知道的是,是否有一种方法可以像GTask以前那样异步运行任何通用的长时间运行的任务。
戴维·普拉内拉

我不知道GTask到底是什么,但是如果您是说gtask.sourceforge.net,那么我认为您不应该使用它。如果还有其他东西,那我就不知道了。但是,听起来您将不得不采用我提到的第二条路线,并实现一些异步API来包装该代码,或者仅在线程中完成所有操作。

问题中有指向它的链接。GTask是(was):chergert.github.com/gtask
David Planella 2012年

1
嗯,看起来非常类似于python-defer提供的API(以及twisted的延期API)。也许您应该考虑使用python-defer?

1
您仍然需要通过使用GLib.idle_add()将调用延迟到主要优先级事件发生之后。像这样:pastebin.ubuntu.com/1011660
dobey 2012年

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之前添加了额外的睡眠,以验证完整更新是否可以在绘制窗口之前运行。我也将其注释掉,以验证线程启动完全不会阻碍窗口绘制。


1
谢谢。我不确定是否可以遵循。首先,我希望slow_load在UI启动后不久就执行该命令,但是除非单击该按钮,否则它似乎从未被调用,这使我有些困惑,因为我认为按钮的目的只是为了提供视觉指示。任务状态。
戴维·普拉内拉

抱歉,我错过了一行。做到了。我忘了告诉GObject为线程做准备。
RobotHumans 2012年

但是,您正在从线程调用主循环,这可能会导致问题,尽管在您的琐碎示例中可能不会轻易暴露这些问题,而这些示例并没有做任何实际的工作。

有道理,但我认为没有一个简单的例子值得通过DBus发送通知(我认为这应该是一个不重要的应用程序)
RobotHumans 2012年

嗯,async_call在此示例中运行对我而言有效,但是当我将其移植到我的应用程序中并添加slow_load我已经拥有的真实功能时,它会带来混乱。
大卫·普拉内拉
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.