python gui - wxPython多執行緒支援

pythontab發表於2013-03-19

如果你經常使用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的主頁,檢視這些方法的使用。


相關文章