【轉】編寫高質量代碼改善C#程序的157個建議——建議87:區分WPF和WinForm的線程模型
建議87:區分WPF和WinForm的線程模型
WPF和WinForm窗體應用程序都有一個要求,那就是UI元素(如Button、TextBox等)必須由創建它的那個線程進行更新。WinForm在這方面的限制並不是很嚴格,所以像下面這樣的代碼,在WinForm中大部分情況下還能運行(本建議後面會詳細解釋為什麽會出現這種現象):
private void buttonStartAsync_Click(object sender, EventArgs e) { Task t = new Task(() => { while (true) { label1.Text = DateTime.Now.ToString(); Thread.Sleep(1000); } }); //如果有異常,就啟動一個新任務 t.ContinueWith((task) => { try { task.Wait(); } catch (AggregateException ex) {foreach (Exception inner in ex.InnerExceptions) { MessageBox.Show(string.Format("異常類型:{0}{1}來自:{2}{3}異常內容:{4}", inner.GetType(),Environment.NewLine, inner.Source, Environment.NewLine, inner.Message)); } } }, TaskContinuationOptions.OnlyOnFaulted); t.Start(); }
但是,相同的一段代碼如果放到WPF環境中,就肯定會拋出System.InvalidOperationException異常。
理論上,WinForm和WPF的線程模型非常接近,它們最後都是調用API(GetMessage或PeekMessage)來處理其他線程發送過來的消息,這些消息存儲在系統的一個消息隊列中。在WinForm和WPF中,創建主界面的線程就是主線程,也就是UI線程,UI線程負責處理該消息隊列。只是兩者在處理消息隊列的上層機制上稍微有一些不同,這就造成了同樣的代碼得到不同的結果。
在WinForm框架中有一個ISynchronizeInvoke接口,所有的UI元素(表現為Control)都繼承了該接口。其中,接口中的InvokdRequired屬性表示了當前線程是否是創建它的線程。接口中的Invoke和BeginInvoke方法負責將消息發送到消息隊列中,這樣,UI線程就能夠正確處理它了。那麽,上面的這段代碼在WinForm上的改進版本為(僅列出While循環部分):
while (true) { if (label1.InvokeRequired) label1.BeginInvoke(new Action(() => { label1.Text = DateTime.Now.ToString(); })); else label1.Text = DateTime.Now.ToString(); Thread.Sleep(1000); }
BeginInvoke方法接受的是一個Delegate類型的參數,在這裏我們用一個Action來實現。
WPF應用程序的線程模型則完全依賴於DispatcherObject類型。所有的WPF控件都繼承自一個抽象類Visual,而這個抽象類又最終繼承自DispatcherObject類型。在這個DispatcherObject類型中有一個屬性,兩個方法。屬性Dispatcher完成所有的工作線程和UI線程之間的調度任務。CheckAccess方法負責檢測工作線程是否可以訪問控件,如果是,則返回True;否則返回False。VerifyAccess方法則負責檢測工作線程是否具有控件的訪問權限,如果不能訪問則拋出異常InvalidOperationException。
WinForm應用程序用類似CheckAccess的方式進行訪問權限的判斷;WPF應用程序則進行了改進,所有的UI控件都采用VerifyAccess的方式進行工作線程訪問權限的判斷。這直接決定了本建議開頭處那個例子的輸出,WPF只要判斷出工作線程和UI線程不是同一個線程的,則直接拋出異常,而WinForm卻有成功執行的余地。但是,WinForm的這種機制直接造成了程序的不穩定,因為即使在大部分情況下代碼能很好的工作,可是在不確定的情況下,那樣的代碼中工作線程會直接操作UI元素,這樣還是會拋出異常的。
考慮到WinForm在這個問題上的局限性,再次對WinForm的線程模型處理進行改進:
//用於表示主線程,在本例中就是UI線程 Thread mainThread; bool CheckAccess() { return mainThread == Thread.CurrentThread; } void VerifyAccess() { if (!CheckAccess()) throw new InvalidOperationException("調用線程無法訪問此對象,因為另一個線程擁有此對象"); } private void buttonStartAsync_Click(object sender, EventArgs e) { //當前線程就是主線程 mainThread = Thread.CurrentThread; Task t = new Task(() => { while (true) { if (!CheckAccess()) label1.BeginInvoke(new Action(() => { label1.Text = DateTime.Now.ToString(); })); else label1.Text = DateTime.Now.ToString(); Thread.Sleep(1000); } }); //如果有異常,就啟動一個新任務 t.ContinueWith((task) => { try { task.Wait(); } catch (AggregateException ex) { foreach (Exception inner in ex.InnerExceptions) { MessageBox.Show(string.Format("異常類型:{0}{1}來自:{2}{3}異常內容:{4}", inner.GetType(), Environment.NewLine, inner.Source, Environment.NewLine, inner.Message)); } } }, TaskContinuationOptions.OnlyOnFaulted); t.Start(); }
在這段代碼中,我們模擬WPF中DispatcherObject的兩個方法CheckAccess和VerifyAccess對線程模型進行了重新處理,增強了系統的穩定性。在實際工作中,我們也可以提取這兩個方法為擴展方法,以便項目中的所有UI類型都能使用到。
WPF支持這兩個方法,其全部代碼如下所示(註意查看While循環部分):
private void buttonStart_Click(object sender, RoutedEventArgs e) { Task t = new Task(() => { while (true) { this.Dispatcher.BeginInvoke(new Action(() => { textBlock1.Text = DateTime.Now.ToString(); })); Thread.Sleep(1000); } }); //為了捕獲異常,啟動了一個新任務 t.ContinueWith((task) => { try { task.Wait(); } catch (AggregateException ex) { foreach (Exception inner in ex.InnerExceptions) { MessageBox.Show(string.Format("異常類型:{0}{1}來自:{2}{3}異常內容:{4}", inner.GetType(), Environment.NewLine, inner.Source, Environment.NewLine, inner.Message)); } } }, TaskContinuationOptions.OnlyOnFaulted); t.Start(); }
註意 為了演示方便,本建議中的異常沒有傳遞到主線程。在實際編碼中,應當始終考慮將異常包裝到主線程。
轉自:《編寫高質量代碼改善C#程序的157個建議》陸敏技
【轉】編寫高質量代碼改善C#程序的157個建議——建議87:區分WPF和WinForm的線程模型