问题描述
我正在编写一个 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 加载而无需等待它完成。Browse the test code。
是否有一种简单且广泛使用的方法来运行异步任务并在完成时收到通知?
最佳答案
您的问题非常常见,因此有很多解决方案(棚屋,具有多处理或线程的队列,工作池……)
由于它非常常见,因此还有一个 Python build-in 解决方案(在 3.2 中,但在此处反向移植:http://pypi.python.org/pypi/futures),称为 parallel.futures。’Futures’ 在许多语言中都可用,因此 Python 以相同的方式调用它们。以下是典型的调用(这是您的 full example ,但是,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,这使得线程无法充分利用多核。所以:不要在 Python 中使用线程!
-
您想要从 DB 返回
slow_load
中的对象不可 pickelable,这意味着它们不能简单地在进程之间传递。因此:没有使用软件中心结果的多处理! -
您调用的库 (softwarecenter.db) 不是线程安全的(似乎包括 gtk 或类似库),因此在线程中调用这些方法会导致奇怪的行为(在我的测试中,从 ‘it works’ 到 ‘core dump’ 再到简单退出而没有结果)。所以:softwarecenter 没有线程。
-
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
来返回可pickelable的结果并使用未来流程。 -
从软件中心切换到 python-apt 或类似版本(您可能不喜欢这样)。但由于您受雇于 Canonical,您可以直接要求软件中心开发人员向其软件添加文档(例如,说明它不是线程安全的),甚至更好,使软件中心成为线程安全的。
需要注意的是:其他人提供的解决方案( Gio.io_scheduler_push_job
、 async_call
)适用于 time.sleep
,但不适用于 softwarecenter.db
。这是因为一切都归结为线程或进程,而线程不能与 gtk 和 softwarecenter
一起使用。
次佳答案
这是使用 GIO 的 I/O 调度程序的另一种选择(我以前从未在 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()