我正在编写一个 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()
我用过任务过去我曾尝试过这种东西,但我担心它的代码 3 年内都没有被触及,更不用说移植到 GObject Introspection 了。最重要的是,它在 Ubuntu 12.04 中不再可用。所以我正在寻找一种简单的方法来异步运行任务,无论是采用标准 Python 方式还是采用 GObject/GTK+ 标准方式。
编辑:下面是一些代码,其中有我尝试执行的操作的示例。我python-defer
按照评论中的建议进行了尝试,但我无法异步运行长任务并让 UI 加载而无需等待它完成。浏览测试代码。
是否有一种简单且广泛使用的方法来运行异步任务并在完成时收到通知?
答案1
您的问题非常常见,因此有很多解决方案(棚屋,具有多处理或线程的队列,工作池......)
由于它非常常见,因此还有一个 python 内置解决方案(在 3.2 中,但在此处反向移植:http://pypi.python.org/pypi/futures) 称为并发.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)
现在来谈谈您的问题,它比您的简单示例要复杂得多。通常,您可以使用线程或进程来解决这个问题,但这就是您的示例如此复杂的原因:
- 大多数 Python 实现都有一个 GIL,这使得线程不是充分利用多核。所以:不要在 Python 中使用线程!
- 您想要从 DB 返回的对象
slow_load
不可 pickelable,这意味着它们不能简单地在进程之间传递。因此:softwarecenter 结果不支持多处理! - 您调用的库 (softwarecenter.db) 不是线程安全的(似乎包括 gtk 或类似库),因此在线程中调用这些方法会导致奇怪的行为(在我的测试中,从“它有效”到“核心转储”再到简单退出而没有结果)。所以: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
以返回可腌制的结果并使用带有流程的未来。 - 从软件中心切换到 python-apt 或类似软件(你可能不喜欢这样)。但由于你受雇于 Canonical,你可以直接要求软件中心开发人员添加文档他们的软件(例如声明它不是线程安全的)甚至更好,使软件中心线程安全。
需要注意的是:其他人给出的解决方案(Gio.io_scheduler_push_job
,async_call
)做可以与 一起使用time.sleep
,但不能与 一起softwarecenter.db
使用。这是因为一切都归结为线程或进程,而线程不能与 gtk 和 一起使用softwarecenter
。
答案2
这是使用 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()
答案3
一旦 GLib Mainloop 完成所有高优先级事件(我相信包括构建 UI),您还可以使用 GLib.idle_add(callback) 来调用长时间运行的任务。
答案4
我认为值得注意的是,这是实现@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 之前添加了额外的睡眠,以验证完整更新是否可以在绘制窗口之前运行。我还将其注释掉,以验证线程启动不会妨碍窗口绘制。