在GUI中进行多线程编程是一件很麻烦的事情,一直以来我都在寻找一个通用的方便的处理方法。在前一段时间中我曾经发表过关于长流程的处理,主要是在处理中插入一个对调度器的处理,而这个调度器使用了队列来实现子线程与主线程之间的数据通信。它的确可以解决一些问题,但并不是非常的方便。那么总结在 wxPython 中所提出的解决多线程问题的答案如下:
- 不要在子线程中进行GUI的更新处理,所有的GUI的更新全部由GUI线程(主线程)来完成
- 使用自定义事件来定义一个事件,然后就可以使用wxPostEvent来发送这个事件,这样会将这个事件放入主线程的事件循环中,从而使用事件得以安全的处理。
- 使用线程安全的队列(Queue)来处理主线程与子线程的进程序通讯。
- 再有wxPython提供了方便的wxCallAfter()方法来实现wxPostEvent的处理。
第一条其实是一个原则。
第二条应该是很常见的方法,它与第四条非常接近。但第四条是一种最简单的情况,有些复杂的情况使用第二条为好。比如:在更新GUI后会有一些返回值,那么这个返回值我需要进一步进行处理。如果使用第四条,则返回值是无法处理的,因此使用第二条则更为方便。而iPodder则主要使用了第二条。它使用了一个通过的类似wxCallAfter的方式,它使用了一个Mixin模块,这个模块创建了一个新的事件并提供了一个Mixin类。这个类可以响应事件,并且有一个线程安装的调用来处理GUI的更新(其实就是使用了wxPostEvent方法)。这个模块的内容为:
import sys
from wxPython.wx import *
from wxPython.lib import neweventDispatchEvent, EVT_DISPATCH = newevent.NewEvent()
class GenericDispatchMixin:
def __init__(self):
EVT_DISPATCH(self, self.OnDispatchEvent)def OnDispatchEvent(self, event):
event.method(*event.arguments)def ThreadSafeDispatch(self, method, *arguments):
wxPostEvent(self, DispatchEvent(method = method, arguments = arguments))
这个类很简单,需要注意的就是这个类本身并不是独立使用的,它需要与一个窗体元素相结合,因为对于事件的绑定处理(此处为EVT_DISPATCH)只有窗体元素才可以做到。因此它可以对自定义事件进行绑定,然后可以对其进行响应处理。响应处理很简单,就是对传入的方法参数进行调用。同时它还提供了对某个方法的线程安全的调用,也就是将需要更新GUI的代码首先封装成方法,然后使用ThreadSafeDispatch()对这个方法进行事件处理。在这个处理过程中我的体会为:
- 事件响应的处理与事件的调用其实是可以分离的。因为事件响应的处理一般在主线程,而事件的调用一般在子线程。如果放在一起,那么需要将这个UI对象作为参数传入子线程中即可。因此在iPodder中有一些代码看上去就象:
self.caller.ThreadSafeDispatch() - 需要将GUI更新的处理封装成方法,然后由主线程进行调用。因此上在调用完GUI的更新处理后其实是进行了事件循环,这样GUI的更新结果并不能直接返回。如果必需要知道GUI的更新结果,则可以通过第三种方式,采用Queue来进行主线程与子线程间的数据交易,从而达到同步。否则可以不理会GUI的更新结果继续处理,而在这种情况下,GUI的更新只是被动地表现子线程的处理。
第三条适合进行主线程与子线程间的同步及数据交换。如果GUI只是被动地改变,使用Queue并不方便。因为采用事件方式你需要写一个事件的响应处理,这样何时被调用是由wxPython来完成的。而使用Queue则做不到,可以还要在某个事件中增加对Queue中数据进行处理的代码,如IDLE事件,但这样并不方便。因此建议只是用来做处理同步及数据交换。而以前我介绍过的长时间处理就是采用Queue方式,现在想一想并不是多么方便的处理。
第四条则是第二条的简化,如果你只是更新GUI,而且不关心更新后的返馈结果,那么使用这条最为方便。
为了测试我修改了以前的longtime.py程序,改用GenericDispatch来处理,代码如下:
#coding=cp936
import wx
import time
import threading
import Queue
import traceback
import sys
from GenericDispatch import GenericDispatchMixinclass MainApp(wx.App):
def OnInit(self):
self.frame = MainFrame()
self.frame.Show(True)
self.SetTopWindow(self.frame)
return Trueclass MainFrame(wx.Frame, GenericDispatchMixin):
def __init__(self):
wx.Frame.__init__(self, None, -1, u’长运行测试’)
GenericDispatchMixin.__init__(self)box = wx.BoxSizer(wx.HORIZONTAL)
self.ID_BTN = wx.NewId()
self.btn = wx.Button(self, self.ID_BTN, u’开始’, size=(60, 22))
box.Add(self.btn, 0, wx.ALIGN_CENTRE|wx.ALL, 0)
wx.EVT_BUTTON(self.btn, self.ID_BTN, self.OnStart)
self.SetSizerAndFit(box)
def OnStart(self, event):
self.progress = wx.ProgressDialog(u"运行…", u"正在处理请稍候…", 100, style=wx.PD_AUTO_HIDE|wx.PD_CAN_ABORT)
self.t = TRun(self)
self.t.setDaemon(True)
self.t.start()
class TRun(threading.Thread):
def __init__(self, caller):
threading.Thread.__init__(self)
self.caller = caller
self.flag = Truedef run(self):
for i in range(100):
print ‘run %d’ % (i+1)
self.caller.ThreadSafeDispatch(self.update, i)
if not self.flag:
self.destroy()
print ‘flag’, self.flag
return
time.sleep(0.1)
self.destroy()def setFlag(self, flag):
self.flag = flagdef update(self, i):
self.flag = self.caller.progress.Update(i+1)
print self.flagdef destroy(self):
self.caller.ThreadSafeDispatch(self.caller.progress.Destroy)
app = MainApp(0)app.MainLoop()
如果你读过以前的Blog,你会发现这段代码不再区别Controller 和View了,而且处理都在子线程中完成,不过它是通过调用self.caller的相关的方法和对象来实现的,因此处理是在子线程中,但真正的元素都是self.caller中的。这样在创建子线程时,将需要处理的UI对象传给子线程,如果处理由子线程来完成,这样写起代码来更方便。要注意的就是在更新时需要调用self.caller的ThreadSafeDispatcah()方法来处理GUI更新的代码。
上面的代码不是最佳的,但是可用的。
这样可以总结一下多线程代码的编写要点:
- 将长流程处理写为线程方式
- 传入要改变的GUI对象
- 在子线程中把对GUI的更新代码写为方法
- 调用GUI对象的ThreadSafeDispatchc()方法来安全地调用GUI的更新方法
- 需要同步或复杂数据交易时采用Queue来处理