天天看點

Winform消息與并行的形象比喻線程列車句柄問題

有一次我給同僚講述跨線程調用時使用了高速行駛的并行列車來比喻,感覺比較形象。

線程列車

多線程就像多個并行的列車,每個線程在各自的軌道上不斷向前行駛。主界面所在的線程稱為UI線程,也叫主線程,主線程依靠消息驅動,可以将主線程的列車每節車廂想象為一個消息,每次轉換并處理一個消息,處理過程中如果有新的消息不會馬上處理而是放入一個消息隊列,等下一輪處理。

例如我在螢幕上點選一個按鈕,作業系統将滑鼠的按下擡起等消息推送到消息隊列中。程式主線程的下一輪開始轉換這個消息然後處理這個消息,發送給指定視窗。假設我們在點選消息處理方法中進行一些界面更新,并調用了Invalidate,此時隻是發出了消息,然後繼續執行後續代碼,當點選消息處理完畢後,才會從消息隊列擷取下一個消息處理。

對于跨線程操作的Invoke,可以這麼了解。就是并行列車在高速行駛中如果直接調用另一個列車上的方法是非常危險的,我們坐車的時候售票員總是提醒我們不要把頭和手伸出窗外是一個道理。是以,假如我們在一個主線程之外的線程列車上想要UI線程去執行一個方法,此時我們需要将方法包裝成委托,然後通過Control.Invoke給主線程發送消息,主線程會在下一次消息處理時處理我們的消息,由被調用的Control在UI線程執行我們的方法。

如果我們在Invoke後,還需要處理傳回值,那麼我們自己所在的列車就不能繼續開了,要停下列車,等主線程的列車處理完我們的方法,傳回結果,并通過消息發送回來,我們收到傳回的消息時,才繼續開動列車處理後續消息。也就是使用Invoke的傳回的WaitHandle的WaitOne方法等待了。

需要了解Windows的消息驅動機制。我們知道任意時刻執行的代碼一定是處于一個消息中,或者是空閑事件消息中。消息也是跨線程調用的基本機制。

Control.BeginInvoke是從線程池啟動一個線程執行,相對主線程是異步的。Control.Invoke則是在其他線程中回到UI線程執行。但這兩種方式都不是推薦的最優做法,推薦用TPL模式,就是使用Task來進行異步。需要回到主線程時用AsyncOperation,原理是一樣的還是發消息,隻是AsyncOperation會發送給一個必定存在的句柄,避免線程安全問題。

句柄問題

另一個很多人不明白的問題就是視窗句柄何時建立,以及OnLoad的時機。其實,Winform程式是對本地代碼的包裝而已,底層還是過程式語言的API調用。過程語言通過句柄來唯一辨別所有的本地資源,所有的方法都需要傳入句柄 。而我們建立的控件類其實并不是真正的可見的類,翻看C++版本的代碼就可以知道,其實還是調用API來CreateWindow,此時傳入的類名才是API中所指的類名,此時傳入的參數在Control裡使用了CreateParam結構體和CreateParam方法來實作。

簡單的說吧,當我們建立一個Button時,隻是調用了Button的構造方法而已,并沒有在螢幕上可見,當我們調用Parent的AddControl時,才會去建立句柄,此時才會觸發控件的OnCreateControl,如果控件是一個UserControl才會觸發OnLoad事件。Control的OnCreateControl和UserControl的OnLoad是同一個時機發生的。隻有建立了句柄才會在螢幕上繪制出來,當父窗體隐藏時,所有子控件的句柄會銷毀,因為不用繪制了,而再次Show時,會重新建立句柄。