C#Winform編程中,跨線程直接更新UI控件的作法是不正確的,會時常出現「線程間操做無效: 從不是建立控件的線程訪問它」的異常。處理跨線程更新Winform UI控件經常使用的方法有4種:
1. 經過UI線程的SynchronizationContext的Post/Send方法更新;
2. 經過UI控件的Invoke/BeginInvoke方法更新;html
3. 經過BackgroundWorker取代Thread執行異步操做;
4. 經過設置窗體屬性,取消線程安全檢查來避免"跨線程操做異常"(非線程安全,建議不使用)。
下文中對以上3種方法應用進行舉例說明,但願能對初識C# Winform的同窗們有些幫助。web
成文表分享交流之意,惶恐水平有限,文中理解和表述有錯誤之處還請你們多被批評指正。編程
更新記錄:安全
2018年2月3日,根據網友評論提示更新錯別字,BegainInvoke=》BeginInvoke。app
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線程發送的消息並是不簡單數據,而是一條委託調用命令。this
//在線程中更新UI(經過UI線程同步上下文m_SyncContext)
m_SyncContext.Post(SetTextSafePost, "This text was set safely by SynchronizationContext-Post.");
能夠這樣解讀這行代碼:向UI線程的同步上下文(m_SyncContext)中提交一個異步消息(UI線程,你收到消息後以異步的方式執行委託,調用方法SetTextSafePost,參數是「this text was ....」).spa
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 取消跨線程調用檢查 |
否 | 同步更新 | 簡單,不建議使用 |
瑞奇特(Jeffrey Richter),《clr var C#》
MSDN, How to: Make Thread-Safe Calls to Windows Forms Controls