相較而言,表單的開啟和關閉是比較耗時的操作。其間涉及到線程、訊息佇列、視窗控制代碼等的建立和銷毀。
另一方面,在其它線程上操作表單依表單的顯示狀態而不同。若在表單開啟或關閉過程未完成之前調用表單方法或設定表單內容,一般將導致異常:
1、表單開啟過程期間,在其它線程上獲得到的InvokeRequired屬性值可能仍為false(此時表單線程尚未開始運行),此時,若直接設定表單的屬性值就將(或可能)出現那個著名的“從不是建立控制項"FrmBusyTip”的線程訪問它”異常(若表單線程已開始運行),若此時關閉表單也將出現異常。
2、表單關閉過程期間,在其它線程上擷取的InvokeRequired屬性可能仍為true(此時表單線程尚未終止),此時,若再使用Invoke函數,則可能出現“建視窗控制代碼之前,不能在控制項上調用 Invoke ”的異常(表單線程已終止)。
因此,在設計Start和Stop介面時,必須考慮到對表單的開啟和關閉過程的同步。
在BusyTipOperator中,對表單開啟和關閉過程的同步方法簡要描述為:
1、Start和Stop方法不能重入,且使用相同的鎖。
2、Start方法直到表單開啟完成才返回,Stop方法直到表單關閉完成後才返回。
這裡的重點在於第二點,而且首先要明確的一點就是如何判斷表單的開啟或關閉操作是否已完成,考慮到在本功能中的需要,我們可大致認為:
1、當在其它線程上擷取的InvokeRequired屬性變為true時,表單開啟完成。
2、當在其它線程上擷取的InvokeRequired屬性變為false時,表單關閉完成。
下面分別描述開啟和關閉過程的同步方法:
一、開啟過程
Start方法通過觸發事件來通知TipThread線程顯示表單,觸發事件的操作即刻完成,然後TipThread才開始顯示表單,因此,Start方法線程必須阻塞直到表單建立完成。
顯而易見,在系統性的操作尚未完成時,至少微軟不會推薦開始執行上層邏輯。考慮到人們一般都會在Form.Load事件中進行業務初始化的工作,因此可以想象直到表單開啟完成後才會觸發Load操作。因此,可在Form.Load響應中喚醒被阻塞的線程。為確定猜想,添加了一句測試代碼:
ShowTipEvent.Set();FrmOpCompletedEvent.WaitOne();Console.WriteLine("InvokeRequired:{0}", FrmTip.InvokeRequired);
在執行結果中未發現有InvokeRequired為false的情況。
另外,阻塞/喚醒可使用兩種方式:一種是Suspend/Wake,一種是使用事件。前一種有這樣的危險:若在Suspend執行前,表單就已經開啟完畢,則可能出現Wake比Suspend先執行的情況。因此,只可使用第二種方式,根據AutoResetEvent的說明“如果沒有等待線程,等待控制代碼將一直保持終止狀態,直到某個線程嘗試等待它,或者直到它的 Reset 方法被調用。”可知,即使AutoResetEvent.Set在AutoResetEvent.Wait之前執行,也不會出現問題。
二、關閉過程
原本以為關閉過程很好同步,因為Close應會待表單關閉完成後才返回,所以使用阻塞函數Invoke來調用Close就應該可以了。然而測試結果卻讓人吃驚:
FrmTip.Invoke(new DelegateInvoke(FrmTip.Close));Console.WriteLine("InvokeRequired:{0} After Close", FrmTip.InvokeRequired);Thread.Sleep(100);Console.WriteLine("InvokeRequired:{0} After Close", FrmTip.InvokeRequired);
這段代碼的第一個輸出是true,第二個輸出則是false。可見Close在表單線程終止前就返回了,因此不得不想辦法進行同步了。
先嘗試與開啟過程一樣的同步方法來同步,在Closed事件中喚醒原線程,發現依然不行。又繞了許多圈想了許多辦法,類似這樣的:
DelegateInvoke invoke = new DelegateInvoke(FrmTip.Close);invoke += this.OpFormCompleted;FrmTip.Invoke(new DelegateInvoke(invoke));FrmOpCompletedEvent.WaitOne();public void OpFormCompleted(){ FrmOpCompletedEvent.Set();}
不過,最終都一一被推翻。許久後,才轉了個彎想到ShowDialog應該會直到表單關閉完成再返回吧。於是便有了TipThreadRun中的:
FrmTip.ShowDialog();
FrmOpCompletedEvent.Set();
這才總算是測試通過了。
以上就是BusyTipOperator中關於表單開啟、關閉過程同步的設計思路和過程。看似不到一百行的代碼,卻費了許多的工夫。最深刻的體會就是:線程同步本就是難事,再牽扯上表單,必定更讓人頭疼。