跨執行緒直接更新UI控制元件彙總
成文表分享交流之意,惶恐水平有限,文中理解和表述有錯誤之處還請大家多被批評指正。
更新記錄:
2018年2月3日,根據網友評論提示更新錯別字,BegainInvoke=》BeginInvoke。
正文
1. 通過UI執行緒的SynchronizationContext的Post/Send方法更新
用法:
//共分三步 //第一步:獲取UI執行緒同步上下文(在窗體建構函式或FormLoad事件中) /// <summary> /// UI執行緒的同步上下文 /// </summary> SynchronizationContext m_SyncContext = null; public Form1() { InitializeComponent(); //獲取UI執行緒同步上下文 m_SyncContext = SynchronizationContext.Current; //Control.CheckForIllegalCrossThreadCalls = false; } //第二步:定義執行緒的主體方法 /// <summary> /// 執行緒的主體方法/// </summary> private void ThreadProcSafePost() { //...執行執行緒任務 //線上程中更新UI(通過UI執行緒同步上下文m_SyncContext) m_SyncContext.Post(SetTextSafePost, "This text was set safely by SynchronizationContext-Post."); //...執行執行緒其他任務 } //第三步:定義更新UI控制元件的方法/// <summary> /// 更新文字框內容的方法 /// </summary> /// <param name="text"></param> private void SetTextSafePost(object text) { this.textBox1.Text = text.ToString(); } //之後,啟動執行緒 /// <summary> /// 啟動執行緒按鈕事件 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void setSafePostBtn_Click(object sender, EventArgs e) { this.demoThread = new Thread(new ThreadStart(this.ThreadProcSafePost)); this.demoThread.Start(); }
說明:三處加粗部分是關鍵。該方法的主要原理是:線上程執行過程中,需要更新到UI控制元件上的資料不再直接更新,而是通過UI執行緒上下文的Post/Send方法,將資料以非同步/同步訊息的形式傳送到UI執行緒的訊息佇列;UI執行緒收到該訊息後,根據訊息是非同步訊息還是同步訊息來決定通過非同步/同步的方式呼叫SetTextSafePost方法直接更新自己的控制元件了。
在本質上,向UI執行緒傳送的訊息並是不簡單資料,而是一條委託呼叫命令。
//線上程中更新UI(通過UI執行緒同步上下文m_SyncContext)
m_SyncContext.Post(SetTextSafePost, "This text was set safely by SynchronizationContext-Post.");
可以這樣解讀這行程式碼:向UI執行緒的同步上下文(m_SyncContext)中提交一個非同步訊息(UI執行緒,你收到訊息後以非同步的方式執行委託,呼叫方法SetTextSafePost,引數是“this text was ....”).
2.通過UI控制元件的Invoke/BeginInvoke方法更新
用法:與方法1類似,可分為三個步驟。
// 共分三步 // 第一步:定義委託型別 // 將text更新的介面控制元件的委託型別 delegate void SetTextCallback(string text); //第二步:定義執行緒的主體方法 /// <summary> /// 執行緒的主體方法 /// </summary> private void ThreadProcSafe() { //...執行執行緒任務 //線上程中更新UI(通過控制元件的.Invoke方法) this.SetText("This text was set safely."); //...執行執行緒其他任務 } //第三步:定義更新UI控制元件的方法 /// <summary> /// 更新文字框內容的方法 /// </summary> /// <param name="text"></param> private void SetText(string text) { // InvokeRequired required compares the thread ID of the // calling thread to the thread ID of the creating thread. // If these threads are different, it returns true. if (this.textBox1.InvokeRequired)//如果呼叫控制元件的執行緒和建立建立控制元件的執行緒不是同一個則為True { while (!this.textBox1.IsHandleCreated) { //解決窗體關閉時出現“訪問已釋放控制代碼“的異常 if (this.textBox1.Disposing || this.textBox1.IsDisposed) return; } SetTextCallback d = new SetTextCallback(SetText); this.textBox1.Invoke(d, new object[] { text }); } else { this.textBox1.Text = text; } } //之後,啟動執行緒 /// <summary> /// 啟動執行緒按鈕事件 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void setTextSafeBtn_Click( object sender, EventArgs e) { this.demoThread = new Thread(new ThreadStart(this.ThreadProcSafe)); this.demoThread.Start(); }
說明:這個方法是目前跨執行緒更新UI使用的主流方法,使用控制元件的Invoke/BeginInvoke方法,將委託轉到UI執行緒上呼叫,實現執行緒安全的更新。原理與方法1類似,本質上還是把執行緒中要提交的訊息,通過控制元件控制代碼呼叫委託交到UI執行緒中去處理。
解決窗體關閉時出現“訪問已釋放控制代碼“的異常部分程式碼參考部落格園-事理同學的文章。
3.通過BackgroundWorker取代Thread執行非同步操作
用法:
//共分三步 //第一步:定義BackgroundWorker物件,並註冊事件(執行執行緒主體、執行UI更新事件) private BackgroundWorker backgroundWorker1 =null; public Form1() { InitializeComponent(); backgroundWorker1 = new System.ComponentModel.BackgroundWorker(); //設定報告進度更新 backgroundWorker1.WorkerReportsProgress = true; //註冊執行緒主體方法 backgroundWorker1.DoWork += new DoWorkEventHandler(backgroundWorker1_DoWork); //註冊更新UI方法 backgroundWorker1.ProgressChanged += new ProgressChangedEventHandler(backgroundWorker1_ProgressChanged); //backgroundWorker1.RunWorkerCompleted += new System.ComponentModel.RunWorkerCompletedEventHandler(this.backgroundWorker1_RunWorkerCompleted); } //第二步:定義執行執行緒主體事件 //執行緒主體方法 public void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) { //...執行執行緒任務 //線上程中更新UI(通過ReportProgress方法) backgroundWorker1.ReportProgress(50, "This text was set safely by BackgroundWorker."); //...執行執行緒其他任務 } //第三步:定義執行UI更新事件 //UI更新方法 public void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e) { this.textBox1.Text = e.UserState.ToString(); } //之後,啟動執行緒 //啟動backgroundWorker private void setTextBackgroundWorkerBtn_Click(object sender, EventArgs e) { this.backgroundWorker1.RunWorkerAsync(); }
說明:C# Winform中執行非同步任務時,BackgroundWorker是個不錯的選擇。它是EAP(Event based Asynchronous Pattern)思想的產物,DoWork用來執行非同步任務,在任務執行過程中/執行完成後,我們可以通過ProgressChanged,ProgressCompleteded事件進行執行緒安全的UI更新。
需要注意的是://設定報告進度更新
backgroundWorker1.WorkerReportsProgress = true;
預設情況下BackgroundWorker是不報告進度的,需要顯示設定報告進度屬性。
4. 通過設定窗體屬性,取消執行緒安全檢查來避免"執行緒間操作無效異常"(非執行緒安全,建議不使用)
用法:將Control類的靜態屬性CheckForIllegalCrossThreadCalls為false。
public Form1() { InitializeComponent(); //指定不再捕獲對錯誤執行緒的呼叫 Control.CheckForIllegalCrossThreadCalls = false; }
說明:通過設定CheckForIllegalCrossThreadCalls屬性,可以指示是否捕獲執行緒間非安全操作異常。該屬性值預設為ture,即執行緒間非安全操作是要捕獲異常的("執行緒間操作無效"異常)。通過設定該屬性為false簡單的遮蔽了該異常。Control.CheckForIllegalCrossThreadCalls的註釋如下
// // 摘要: // 獲取或設定一個值,該值指示是否捕獲對錯誤執行緒的呼叫,這些呼叫在除錯應用程式時訪問控制元件的 System.Windows.Forms.Control.Handle // 屬性。 // // 返回結果: // 如果捕獲了對錯誤執行緒的呼叫,則為 true;否則為 false。 [EditorBrowsable(EditorBrowsableState.Advanced)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] [SRDescription("ControlCheckForIllegalCrossThreadCalls")] [Browsable(false)] public static bool CheckForIllegalCrossThreadCalls { get; set; }
綜述:
文中介紹的4種方法,前三種是執行緒安全的 ,可在實際專案中因地制宜的使用。最後一種方法是非執行緒安全的,初學者可以實驗體會但不建議使用它。
下面列表對比一下這四種方法
方法 | 執行緒安全 | 支援非同步/同步 | 其他 |
UI SyncContext更新 | 是 | Post/Send | 儘量在窗體建構函式、FormLoad中獲取同步上下文 |
控制元件Invoke | 是 | control.Invoke/BeginInvoke | 注意檢查控制元件控制代碼是否已釋放 |
BackgroundWorker更新 | 是 |
ProgressChanged、RunWorkerCompleted 事件同步更新 |
報告進度 |
CheckForIllegalCrossThreadCalls 取消跨執行緒呼叫檢查 |
否 | 同步更新 | 簡單,不建議使用 |