【基礎】多線程更新窗體UI的若干方法

前言

在單線程中設置窗體某個控件的值很簡單的事,只須要設置控件文本的值就能夠了,可是有的業務場景非常複雜,界面上的控件也不少,這種狀況下當數據量比較多的時候,在單線程中更新UI不可避免地會發生假死或卡頓現象,用戶體驗十分不爽,因此必須採用多線程來處理數據和UI。可是若是直接添加一個線程來更新控件信息,就會拋出錯誤,很顯然微軟並不但願咱們這樣作,由於UI控件不是線程安全的,若是隨意地在任何線程中改變控件的值,會發生各類奇怪的問題,多個線程間會爭奪資源,沒有秩序地更改控件的值,顯然這是咱們不想看到的結果。固然,解決這個問題的方式有不少,在此列出幾種經常使用的方法,作一下記錄,溫故而知新。html

不檢測線程間衝突

 因爲業務的變動,致使界面控件和數據有所增長,單線程狀況下性能堪憂,這時就須要從單線程切換到多線程,咱們寫的代碼多是這樣:安全

 1 private void btnSetOrg_Click(object sender, EventArgs e)
 2 {
 3     Thread t = new Thread(new ParameterizedThreadStart(SetOrgValue));
 4     //Thread t = new Thread(SetOrgValue); 效果同上
 5     t.Start("Organization");
 6 }
 7 
 8 void SetOrgValue(object obj)
 9 {
10     this.Org.Text = obj.ToString();
11 }

上述代碼建立一個新的線程,並在新的線程中爲Org控件賦值,看起來沒什麼問題,但當咱們運行的時候,會報一個錯誤:線程間操做無效: 從不是建立控件「Org」的線程訪問它。就是說上述代碼中的t不是建立TextBox的線程,因此不能訪問Org控件的屬性。默認只能由建立該控件的線程去訪問該控件的數據,不然會致使讀寫不一致。操做系統中介紹過PV操做,對此有很好的解釋。對此有一個極爲簡單的方法能夠避免上述錯誤,那就是不檢測線程間衝突,只須要在構造函數中添加一行代碼便可:多線程

 1 public UIForm()
 2 {
 3 
 4     InitializeComponent();
 5     Control.CheckForIllegalCrossThreadCalls = false; //不檢測跨線程調用
 6 }
 7 
 8 private void btnSetOrg_Click(object sender, EventArgs e)
 9 {
10     Thread t = new Thread(new ParameterizedThreadStart(SetOrgValue));
11     //Thread t = new Thread(SetOrgValue); 效果同上
12     t.Start("Organization");
13 }
14 
15 
16 void SetOrgValue(object obj)
17 {
18     this.Org.Text = obj.ToString();
19 }

這段代碼顯然是爲了應付線程間操做無效的錯誤,關閉了跨線程訪問UI的檢測,容許多線程隨意更改控件數據,但最後控件的屬性是什麼值,只有天知道,因此並不提倡使用該方法。函數

經常使用的多線程訪問UI控件方式

  • 利用Delegate調用
  • 利用BackgroundWorker
  • 利用SynchronizationContext上下文
  • 利用Dispatcher.BeginInvoke

首先說說委託調用的方式更新UI控件的值。每一個控件都有一個Bool類型的InvokeRequired屬性,表示該控件是否存在建立該控件之外的線程要訪問該控件,若是值爲True說明存在這樣的線程,那麼就須要利用委託調用來更新控件的值。把第二部分的代碼變動以下:性能

 1 delegate void SetValue(object obj);
 2 
 3 private void btnSetOrg_Click(object sender, EventArgs e)
 4 {
 5     Thread t = new Thread(new ParameterizedThreadStart(SetOrgValue));
 6     //Thread t = new Thread(SetOrgValue); 效果同上
 7     t.Start("Organization");
 8 }
 9 
10 private void UpdateOrg(object obj)
11 {
12     
13     if (Org.InvokeRequired)//若是是非建立該控件的線程訪問該控件
14     {
15         SetValue set = SetOrgValue;
16         Org.Invoke(set, obj);
17     }
18     else
19     {
20         Org.Text = obj.ToString();
21     }
22 }
23 
24 private void SetOrgValue(object obj)
25 {
26     this.Org.Text = obj.ToString();
27 }

採用委託的方式,首先要檢查訪問控件的線程是不是建立該控件的線程,若是不是的話,就採用委託的方式去調用設值的方法。若是從另外一個線程調用控件的方法,那麼必須使用控件的Invoke方法來將調用封送到適當的線程。網上有一個有趣的比喻,若是某人向你(Org控件)借錢(訪問並修改),若是直接去你的錢包拿(設置Org的屬性值)是很不安全的,萬一直接搶走了呢?因此須要提早告訴你一聲說我須要借錢(委託),而後本身拿出錢(線程安全)借給借錢的人。這樣理解起來就比較容易接受了。ui

第二種方式是利用BackgroundWorker來訪問控件,其實質上也是建立了新線程,只不過BackgroundWorker作了一個封裝,使咱們用起來更方便一些。this

 1 private void btnSetOrg_Click(object sender, EventArgs e)
 2 {
 3     using (BackgroundWorker bw = new BackgroundWorker())
 4     {
 5         bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(RunWorkerCompleted);
 6         bw.DoWork += new DoWorkEventHandler(DoWork);
 7         bw.RunWorkerAsync("Organization");
 8     }
 9 }
10 
11 void RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
12 {
13     //這時後臺線程已經完成,並返回了主線程,因此能夠直接使用UI控件了
14     this.textBox1.Text = e.Result.ToString();
15     MessageBox.Show(Thread.CurrentThread.ManagedThreadId.ToString());//查看當前線程的ID
16 }       
17 
18 void DoWork(object sender, DoWorkEventArgs e)
19 {
20     MessageBox.Show(Thread.CurrentThread.ManagedThreadId.ToString());//查看當前線程的ID
21     e.Result = e.Argument;//這裏只是簡單的把參數當作結果返回,固然也能夠在這裏作複雜的處理後,再返回本身想要的結果(這裏的操做是在另外一個線程上完成的)
22 }

第三種方式就是利用SynchronizationContext上下文。這種方式相對於上面的幾種方式不是很經常使用。使用方式以下:spa

 1 private void btnSet_Click(object sender, EventArgs e)
 2 {
 3     Thread t = new Thread(new ParameterizedThreadStart(Run));
 4     OrgParam _org = new OrgParam() { Context = SynchronizationContext.Current, Org = "Organization" };
 5     t.Start(_org);
 6 }
 7 void Run(object obj) 
 8 {
 9     OrgParam org = obj as OrgParam;
10     org.Context.Post(SetTextValue, org.Org);
11 }
12 
13 void SetTextValue(object obj) 
14 {
15     this.Org.Text = obj.ToString();
16 }
17 public class OrgParam 
18 {
19     public SynchronizationContext Context { set; get; }
20     public object Org { set; get; }
21 }

最後一種就是利用Dispatcher.BeginInvoke來更新控件的值。這種方式相對來講比較方便,可是也不是很經常使用。操作系統

1 private void btnSetOrg_Click(object sender, EventArgs e)
2 {
3    Thread t = new Thread(new ParameterizedThreadStart(SetOrgValue));
4    t.Start("Orgnization");
5 }
6 private void SetOrgValue(object obj)
7 {
8     this.Dispatcher.BeginInvoke(() => { this.txt.Text = text.ToString(); }); 
9 }

總結

以上就是多線程下更新控件值的幾種方式,雖然方式不同,但都是爲了解決多線程訪問控件出現的問題。其中不檢測線程間衝突的方法和採用委託的方式只限於WinForm下使用;而最後一種Dispatcher.BeginInvoke只限於Silverlight下使用;而BackgroundWorkerSynchronizationContext這兩種方式是適用於Winform/Silverlight的。之前在寫WinForm的時候,只知道採用委託的方式和BackgroundWorker來解決跨線程訪問控件的問題,如今回頭看看這塊的內容,又知道了幾種解決問題的方式,也許這就是孔子老人家所說的「溫故而知新」吧。立刻就要過年了,提早祝你們新年快樂!線程

 

做者:悠揚的牧笛

博客地址:http://www.cnblogs.com/xhb-bky-blog/p/6262790.html

聲明:本博客原創文字只表明本人工做中在某一時間內總結的觀點或結論,與本人所在單位沒有直接利益關係。非商業,未受權貼子請以現狀保留,轉載時必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接。

相關文章
相關標籤/搜索