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時,會重新創建句柄。

浙公網安備 33010602011771號