[摘要]本文介紹c#
winform多線程開發之control.invoke,并提供詳細的示例代碼供參考。
下面我們就把在windows form軟體中使用invoke時的多線程要注意的問題給大家做一個介紹。
首先,什麼樣的操作需要考慮使用多線程?總的一條就是,負責與使用者互動的線程(以下簡稱為ui線程)應該保持順暢,當ui線程調用的api可能引起阻塞時間超過30毫秒時(比如通路cd-rom等速度超慢的外設、進行遠端調用等等)就應該考慮使用多線程。為什麼是30毫秒?30毫秒的概念是人眼可以察覺到的一個遲滞,大約等同于電影裡的一幀停留的時間,最長不要超過100毫秒。
第二,最友善和簡單的多線程是使用線程池。通過線程池裡的線程運作代碼的最簡便方法則是使用異步委托調用。注意委托調用通常是同步完成的,請使用begininvoke方法,這樣就可以把要調用的方法排隊到線程池裡等候處理,而程式的流程會立刻傳回到調用方(此處是ui線程),而調用方是以不會出現阻塞。
看看下面的例子我們就發現要使用線程池異步執行代碼也并非十分複雜,這裡我們利用system.windows.forms.methodinvoker委托進行異步調用。注意methodinvoker委托不接受方法參數,如果需要向異步執行的方法傳遞參數,請使用其他委托,或者需要自己定義。
歸納上述方法,對ui線程而言實際上就是:1、發出調用,2、立刻傳回,具體運作過程不理了,這樣ui線程就不會被阻塞。這種方法很重要,下面我們會深入介紹。除了上面的方法,還有其他使用線程池的方法,當然如果你高興也可以自己建立線程。
第三,在windows form中使用多線程的,最重要的一條注意事項是,除了建立控件的線程以外,絕對不要在任何其他線程裡面調用控件的成員(隻有極個别情況例外),也就是說控件屬于建立它的線程,不能從其他線程裡面通路。這一條适用于所有從system.windows.forms.control派生的控件(是以可以說是幾乎所有控件),包括form控件本身也是。舉一反三,我們很容易得出這樣的結論,控件的子控件必須由建立控件的線程來建立,比如一個表單上的按鈕,比如由建立表單的線程來建立,是以,一個視窗中的所有控件實際上都活在同一個線程之中。在實際程式設計時,大多數的軟體的做法都是讓同一線程負責全部的控件,這就是我們所說的ui線程。看下面的例子:
我們要特别提醒大家,很多人剛開始的時候都會使用以上的方法來通路不在同一個線程裡的控件(包括筆者本人),而且在1.0版.net 架構上似乎沒有發現問題,但是這根本就是錯的,更糟糕的是,程式員在這裡不會得到任何錯誤提示,一開始就上當受騙,之後會莫明其妙地發現其他錯誤,這就是windows form多線程程式設計的痛苦所在。筆者試過花很多時間來debug自己寫的splash視窗突然消失的問題,結果還是失敗了:筆者在軟體的引導過程中,用另外一個線程裡建立了一個splash視窗來顯示歡迎資訊,然後嘗試把主線程裡引導的狀态直接寫入到splash視窗上的控件中,開始還ok,可是過一會splash視窗就莫明其妙消失了。
了解了這一點,我們應該留意到,有時候即使沒有用system.threading.thread來顯式建立一個線程,我們也可能因為使用了異步委托的begininvoke方法來隐式建立了線程(從線程池裡),在這種線程裡也同樣不能調用ui線程所建立的控件的成員。
第四,由于上述限制,我們可能會感到很不友善,的确,當我們利用一個新建立的線程來執行某些花時間的運算時,怎樣知道運算進度如何并通過ui反映給使用者呢?解決方法很多!比如熟悉多線程程式設計的使用者很快會想到,我們采用一些低級的同步方法,工作者線程把狀态儲存到一個同步對象中,讓ui線程輪詢(polling)該對象并回報給使用者就可以了。不過,這還是挺麻煩的,實際上不用這樣做,control類(及其派生類)對象有一個invoke方法很特别,這是少數幾個不受線程限制的成員之一。我們前面說到,絕對不要在任何其他線程裡面調用非本線程建立的控件的成員時,也說了“隻有極個别情況例外”,這個invoke方法就是極個别情況之一----invoke方法可以從任何線程裡面調用。下面我們來講解invoke方法。
invoke方法的參數很簡單,一個委托,一個參數表(可選),而invoke方法的主要功能就是幫助你在ui線程(即建立控件的線程)上調用委托所指定的方法。invoke方法首先檢查發出調用的線程(即目前線程)是不是ui線程,如果是,直接執行委托指向的方法,如果不是,它将切換到ui線程,然後執行委托指向的方法。不管目前線程是不是ui線程,invoke都阻塞直到委托指向的方法執行完畢,然後切換回發出調用的線程(如果需要的話),傳回。注意,使用invoke方法時,ui線程不能處于阻塞狀态。以下msdn裡關于invoke方法的說明:
好了,說完invoke,順便說說begininvoke,毫無疑問這是invoke的異步版本(invoke是同步完成的),不過大家不要和上面的system.windows.forms.methodinvoker委托中的begininvoke混淆,兩者都是利用不同線程來完成工作,但是控件的begininvoke方法總是使用ui線程,而其他的異步委托調用方法則是利用線程池裡的線程。相對invoke而言,使用begininvoke稍稍麻煩一點,但還是那句話,異步比同步效果好,盡管複雜些。比如同步方法可能出現這樣一種死鎖情況:工作者線程通過invoke同步調用ui線程裡的方法時會阻塞,而萬一ui線程正在等待工作者線程做某件事時怎麼辦?是以,能夠使用異步方法時應盡量使用異步方法。
下面我們利用所學到的知識來改寫上面那個簡單的例子:
第五,關于多線程程式設計還要考慮線程之間的同步問題、死鎖和争用條件,有關這類問題的文章很多,我們就不贅述了