如果你经常使用python开发gui程序的话,那么就知道,有时你需要很长时间来执行一个任务。当然,如果你使用命令行程序来做的话,你回非常惊讶。大部分情况下,这会堵塞gui的事件循环,用户会看到程序卡死。如何才能避免这种情况呢?当然是利用线程或进程了!本文,我们将探索如何使用wxpython和theading模块来实现。
wxpython线程安全方法
wxpython中,有三个“线程安全”的函数。如果你在更新ui界面时,三个函数都不使用,那么你可能会遇到奇怪的问题。有时gui也忙运行挺正常,有时却会无缘无故的崩溃。因此就需要这三个线程安全的函数:wx.postevent, wx.callafter和wx.calllater。据robin dunn(wxpython作者)描述,wx.callafter使用了wx.postevent来给应用程序对象发生事件。应用程序会有个事件处理程序绑定到事件上,并在收到事件后,执行处理程序来做出反应。我认为wx.calllater是在特定时间后调用了wx.callafter函数,已实现规定时间后发送事件。
robin dunn还指出python全局解释锁 (gil)也会避免多线程同时执行python字节码,这会限制程序使用cpu内核的数量。另外,他还说,“wxpython发布gil是为了在调用wx api时,其他线程也可以运行”。换句话说,在多核机器上使用多线程,可能效果会不同。
总之,大概的意思是桑wx函数中,wx.calllater是最抽象的线程安全函数, wx.callafter次之,wx.postevent是最低级的。下面的实例,演示了如何使用wx.callafter和wx.postevent函数来更新wxpython程序。
wxpython, theading, wx.callafter and pubsub
wxpython邮件列表中,有些专家会告诉其他人使用wx.callafter,并利用pubsub实现wxpython应用程序与其他线程进行通讯,我也赞成。如下代码是具体实现:
import time
import wx
from threading import thread
from wx.lib.pubsub import publisher
########################################################################
class testthread(thread):
“””test worker thread class.”””
#———————————————————————-
def __init__(self):
“””init worker thread class.”””
thread.__init__(self)
self.start() # start the thread
#———————————————————————-
def run(self):
“””run worker thread.”””
# this is the code executing in the new thread.
for i in range(6):
time.sleep(10)
wx.callafter(self.posttime, i)
time.sleep(5)
wx.callafter(publisher().sendmessage, “update”, “thread finished!”)
#———————————————————————-
def posttime(self, amt):
“””
send time to gui
“””
amtoftime = (amt + 1) * 10
publisher().sendmessage(“update”, amtoftime)
########################################################################
class myform(wx.frame):
#———————————————————————-
def __init__(self):
wx.frame.__init__(self, none, wx.id_any, “tutorial”)
# add a panel so it looks the correct on all platforms
panel = wx.panel(self, wx.id_any)
self.displaylbl = wx.statictext(panel, label=”amount of time since thread started goes here”)
self.btn = btn = wx.button(panel, label=”start thread”)
btn.bind(wx.evt_button, self.onbutton)
sizer = wx.boxsizer(wx.vertical)
sizer.add(self.displaylbl, 0, wx.all|wx.center, 5)
sizer.add(btn, 0, wx.all|wx.center, 5)
panel.setsizer(sizer)
# create a pubsub receiver
publisher().subscribe(self.updatedisplay, “update”)
#———————————————————————-
def onbutton(self, event):
“””
runs the thread
“””
testthread()
self.displaylbl.setlabel(“thread started!”)
btn = event.geteventobject()
btn.disable()
#———————————————————————-
def updatedisplay(self, msg):
“””
receives data from thread and updates the display
“””
t = msg.data
if isinstance(t, int):
self.displaylbl.setlabel(“time since thread started: %s seconds” % t)
else:
self.displaylbl.setlabel(“%s” % t)
self.btn.enable()
#———————————————————————-
# run the program
if __name__ == “__main__”:
app = wx.pysimpleapp()
frame = myform().show()
app.mainloop()
我们会用time模块来模拟耗时过程,请随意将自己的代码来代替,而在实际项目中,我用来打开adobe reader,并将其发送给打印机。这并没什么特别的,但我不用线程的话,应用程序中的打印按钮就会在文档发送过程中卡住,ui界面也会被挂起,直到文档发送完毕。即使一秒,两秒对用户来说都有卡的感觉。
总之,让我们来看看是如何工作的。在我们编写的thread类中,我们重写了run方法。该线程在被实例化时即被启动,因为我们在__init__方法中有“self.start”代码。run方法中,我们循环6次,每次sheep10秒,然后使用wx.callafter和pubsub更新ui界面。循环结束后,我们发送结束消息给应用程序,通知用户。
你会注意到,在我们的代码中,我们是在按钮的事件处理程序中启动的线程。我们还禁用按钮,这样就不能开启多余的线程来。如果我们让一堆线程跑的话,ui界面就会随机的显示“已完成”,而实际却没有完成,这就会产生混乱。对用户来说是一个考验,你可以显示线程pid,来区分线程,你可能要在可以滚动的文本控件中输出信息,这样你就能看到各线程的动向。
最后可能就是pubsub接收器和事件的处理程序了:
def updatedisplay(self, msg):
“””
receives data from thread and updates the display
“””
t = msg.data
if isinstance(t, int):
self.displaylbl.setlabel(“time since thread started: %s seconds” % t)
else:
self.displaylbl.setlabel(“%s” % t)
self.btn.enable()
看我们如何从线程中提取消息,并用来更新界面?我们还使用接受到数据的类型来告诉我们什么显示给了用户。很酷吧?现在,我们玩点相对低级一点点,看wx.postevent是如何办的。
wx.postevent与线程
下面的代码是基于wxpython wiki编写的,这看起来比wx.callafter稍微复杂一下,但我相信我们能理解。
import time
import wx
from threading import thread
# define notification event for thread completion
evt_result_id = wx.newid()
def evt_result(win, func):
“””define result event.”””
win.connect(-1, -1, evt_result_id, func)
class resultevent(wx.pyevent):
“””simple event to carry arbitrary result data.”””
def __init__(self, data):
“””init result event.”””
wx.pyevent.__init__(self)
self.seteventtype(evt_result_id)
self.data = data
########################################################################
class testthread(thread):
“””test worker thread class.”””
#———————————————————————-
def __init__(self, wxobject):
“””init worker thread class.”””
thread.__init__(self)
self.wxobject = wxobject
self.start() # start the thread
#———————————————————————-
def run(self):
“””run worker thread.”””
# this is the code executing in the new thread.
for i in range(6):
time.sleep(10)
amtoftime = (i + 1) * 10
wx.postevent(self.wxobject, resultevent(amtoftime))
time.sleep(5)
wx.postevent(self.wxobject, resultevent(“thread finished!”))
########################################################################
class myform(wx.frame):
#———————————————————————-
def __init__(self):
wx.frame.__init__(self, none, wx.id_any, “tutorial”)
# add a panel so it looks the correct on all platforms
panel = wx.panel(self, wx.id_any)
self.displaylbl = wx.statictext(panel, label=”amount of time since thread started goes here”)
self.btn = btn = wx.button(panel, label=”start thread”)
btn.bind(wx.evt_button, self.onbutton)
sizer = wx.boxsizer(wx.vertical)
sizer.add(self.displaylbl, 0, wx.all|wx.center, 5)
sizer.add(btn, 0, wx.all|wx.center, 5)
panel.setsizer(sizer)
# set up event handler for any worker thread results
evt_result(self, self.updatedisplay)
#———————————————————————-
def onbutton(self, event):
“””
runs the thread
“””
testthread(self)
self.displaylbl.setlabel(“thread started!”)
btn = event.geteventobject()
btn.disable()
#———————————————————————-
def updatedisplay(self, msg):
“””
receives data from thread and updates the display
“””
t = msg.data
if isinstance(t, int):
self.displaylbl.setlabel(“time since thread started: %s seconds” % t)
else:
self.displaylbl.setlabel(“%s” % t)
self.btn.enable()
#———————————————————————-
# run the program
if __name__ == “__main__”:
app = wx.pysimpleapp()
frame = myform().show()
app.mainloop()
让我们先稍微放一放,对我来说,最困扰的事情是第一块:
# define notification event for thread completion
evt_result_id = wx.newid()
def evt_result(win, func):
“””define result event.”””
win.connect(-1, -1, evt_result_id, func)
class resultevent(wx.pyevent):
“””simple event to carry arbitrary result data.”””
def __init__(self, data):
“””init result event.”””
wx.pyevent.__init__(self)
self.seteventtype(evt_result_id)
self.data = data
evt_result_id只是一个标识,它将线程与wx.pyevent和“evt_result”函数关联起来,在wxpython代码中,我们将事件处理函数与evt_result进行捆绑,这就可以在线程中使用wx.postevent来将事件发送给自定义的resultevent了。
结束语
希望你已经明白在wxpython中基本的多线程技巧。还有其他多种多线程方法这里就不在涉及,如wx.yield和queues。幸好有wxpython wiki,它涵盖了这些话题,因此如果你有兴趣可以访问wiki的主页,查看这些方法的使用。