背景描述:近期為現場編寫了一個數據處理工具,因數據量較大,執行時間超過1小時,為了增強使用體驗,采用多線程方式防止主界面卡死並且在主界面上實時打印當前執行信息。
遇到問題:在子線程中,因需要實時返回當前執行信息到主線程界面展示,如果處理不當會出現【線程間操作無效: 從不是創建控件XXX的線程訪問它】
解決方法:
看了網上的部分資料,發現可以通過幾種方式來實現子線程更新主線程的控件信息,下面分別來看一下:
1、過UI線程的SynchronizationContext的Post/Send方法更新
方法的主要原理是:在線程執行過程中,需要更新到UI控件上的數據不再直接更新,而是通過UI線程上下文的Post/Send方法,將數據以異步/同步消息的形式發送到UI線程的消息隊列;UI線程收到該消息后,根據消息是異步消息還是同步消息來決定通過異步/同步的方式調用SetTextSafePost方法直接更新控件。在本質上,向UI線程發送的消息並是不簡單數據,而是一條委托調用命令。
m_SyncContext.Post(SetTextSafePost,msg)可以理解為:向UI線程的同步上下文(m_SyncContext)中提交一個異步消息(UI線程,你收到消息后以異步的方式執行委托,調用方法SetTextSafePost,參數是msg)。
#region 通過UI線程的SynchronizationContext的Post/Send方法更新UI /// <summary> /// 1、定義UI線程的同步上下文 /// </summary> SynchronizationContext m_SyncContext = null; /// <summary> /// 3、定義線程的主體方法 /// </summary> private void ThreadProcSafePost() { for (int i = 0; i < 10; i++) { //在線程中更新UI(通過UI線程同步上下文m_SyncContext) m_SyncContext.Post(SetTextSafePost, DateTime.Now.ToString()); //暫停1s Thread.Sleep(1000); } //完成后發送相關信息 m_SyncContext.Post(SetTextSafePost, "操作完成"); } /// <summary> /// 4、更新UI方法 /// </summary> /// <param name="text"></param> private void SetTextSafePost(object str) { memoEdit_main.Text = memoEdit_main.Text + "\r\n" + str.ToString(); memoEdit_main.SelectionStart = memoEdit_main.Text.Length; memoEdit_main.ScrollToCaret(); Application.DoEvents(); } /// <summary> /// 2、獲取UI線程同步上下文(建議在窗體構造函數或FormLoad事件中,這里因為測試方便,直接放在了這里) /// 啟動線程 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btn_sync_Click(object sender, EventArgs e) { //獲取UI線程同步上下文 m_SyncContext = SynchronizationContext.Current; Thread thread = new Thread(new ThreadStart(this.ThreadProcSafePost)); thread.Start(); } #endregion
2、通過Invoke/BeginInvoke方法實現(推薦)
使用Invoke/BeginInvoke更新主線程UI的情況比較多,通過委托實現線程安全。原理和方法1類似,本質上還是把線程中要提交的消息,通過控件句柄調用委托交到UI線程中去處理。
#region 使用INVOKE方法通過子線程更新主線程控件 /// <summary> /// 1、定義界面更新操作 /// 更新Memoedit信息並定位到行尾 /// </summary> /// <param name="strMsg"></param> private void MainThreadUIOper(string strMsg) { memoEdit_main.Text = memoEdit_main.Text + "\r\n" + strMsg; memoEdit_main.SelectionStart = memoEdit_main.Text.Length; memoEdit_main.ScrollToCaret(); Application.DoEvents(); } /// <summary> /// 2、定義委托事件,用於委托上一步定義的MainThreadUIOper /// </summary> /// <param name="str"></param> public delegate void UIOperDelegate(string str); /// <summary> /// 3、定義子線程執行操作函數 /// </summary> /// <param name="strMsg"></param> private void DoWork(object para) { string strPara = para.ToString(); for (int i = 0; i < 10; i++) { //注意BeginInvoke和Invoke的區別,根據實際情況選用,前者為異步,后者為同步 //也可以將new UIOperDelegate(MainThreadUIOper)提出來單獨定義 UIOperDelegate temp = new UIOperDelegate(MainThreadUIOper); 后面公共引用 this.Invoke(new UIOperDelegate(MainThreadUIOper), new object[] { DateTime.Now.ToString() }); //暫停1s Thread.Sleep(1000); } //完成后發送相關信息 this.BeginInvoke(new UIOperDelegate(MainThreadUIOper), new Object[] { "測試完成!" }); //使用 MessageBoxOptions.ServiceNotification是的窗口始終在最上一層顯示 MessageBox.Show("提示", "測試完成!", MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1, MessageBoxOptions.ServiceNotification); } /// <summary> /// 4、在主線程中開啟子線程並傳遞參數給子線程 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btn_start_Click(object sender, EventArgs e) { memoEdit_main.Text = ""; memoEdit_main.Update(); //注意,如果需要給子線程傳遞參數,需要使用ParameterizedThreadStart,否則使用ThreadStart即可,后面直接thread.Start(); Thread thread = new Thread(new ParameterizedThreadStart(DoWork)); thread.Start("para"); } #endregion
3、通過BackgroundWorker取代Thread執行異步操作
通過BackgroundWorker進行異步操作,將線程操作封裝到BackgroundWorker中,這樣邏輯更加簡單,使用更加清晰,對於比較簡單的任務,建議可以采用此種方式。
#region 使用BackgroundWorker方法進行異步操作 /// <summary> /// 1、定義BackgroundWorker對象 /// </summary> private BackgroundWorker m_backgroundWorker =null; /// <summary> /// 2、主體方法,定義子操作函數 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> public void m_backgroundWorker_DoWork(object sender, DoWorkEventArgs e) { for (int i = 0; i < 10; i++) { //在線程中更新UI(通過ReportProgress方法) m_backgroundWorker.ReportProgress(50, DateTime.Now.ToString()); //暫停1s Thread.Sleep(1000); } //完成后發送相關信息 m_backgroundWorker.ReportProgress(100, "操作完成"); } /// <summary> /// 3、定義執行UI更新事件 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> public void m_backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e) { memoEdit_main.Text = memoEdit_main.Text + "\r\n" + e.UserState.ToString(); memoEdit_main.SelectionStart = memoEdit_main.Text.Length; memoEdit_main.ScrollToCaret(); Application.DoEvents(); } /// <summary> /// 4、注冊事件(執行線程主體、執行UI更新事件) /// 啟動子線程 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btn_prop_Click(object sender, EventArgs e) { m_backgroundWorker = new System.ComponentModel.BackgroundWorker(); //設置報告進度更新,注意此選項必須設置為true,否則無法返回進度 m_backgroundWorker.WorkerReportsProgress = true; //注冊線程主體方法 m_backgroundWorker.DoWork += new DoWorkEventHandler(m_backgroundWorker_DoWork); //注冊更新UI方法 m_backgroundWorker.ProgressChanged += new ProgressChangedEventHandler(m_backgroundWorker_ProgressChanged); //注冊子線程執行完成事件 m_backgroundWorker.RunWorkerCompleted += new System.ComponentModel.RunWorkerCompletedEventHandler(this.m_backgroundWorker_RunWorkerCompleted); this.m_backgroundWorker.RunWorkerAsync(); } /// <summary> /// 子線程執行完成后操作 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> public void m_backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { MessageBox.Show("操作完成"); } #endregion
4、通過屬性設置,取消線程安全檢查實現(不建議使用)
此方法只是不再捕獲線程之間安全操作異常,是非線程安全的,不建議在實際中使用。
public FormMain()
{
InitializeComponent();
//指定不再捕獲對錯誤線程的調用
Control.CheckForIllegalCrossThreadCalls = false;
}
總結:
介紹了4種方法,前三種是線程安全的 ,可在實際項目中因地制宜的使用。最后一種方法是非線程安全的,不建議使用它。
下面列表對比一下這四種方法 :
因附件上傳不方便,下面把整個代碼貼出來如下:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; namespace multiThread { public partial class FormMain : Form { public FormMain() { InitializeComponent(); //指定不再捕獲對錯誤線程的調用 Control.CheckForIllegalCrossThreadCalls = false; } #region 最基本的多線程調用 private void btn_Update_Click(object sender, EventArgs e) { Thread t = new Thread(new ThreadStart(DataUpdate)); t.Start(); } /// <summary> /// 數據處理 /// </summary> private void DataUpdate() { //通過循環,模擬數據處理過程 for (int i = 0; i < 10; i++) { //在未設置CheckForIllegalCrossThreadCalls情況下,如果直接操作memoEdit_main,則會彈出【線程間操作無效: 從不是創建控件XXX的線程訪問它】錯誤 memoEdit_main.Text = memoEdit_main.Text + "\r\n" + DateTime.Now.ToString(); //MessageBox.Show(DateTime.Now.ToString()); //暫停1s Thread.Sleep(1000); } MessageBox.Show("處理完成!"); } #endregion #region 使用INVOKE方法通過子線程更新主線程控件 /// <summary> /// 1、定義界面更新操作 /// 更新Memoedit信息並定位到行尾 /// </summary> /// <param name="strMsg"></param> private void MainThreadUIOper(string strMsg) { memoEdit_main.Text = memoEdit_main.Text + "\r\n" + strMsg; memoEdit_main.SelectionStart = memoEdit_main.Text.Length; memoEdit_main.ScrollToCaret(); Application.DoEvents(); } /// <summary> /// 2、定義委托事件,用於委托上一步定義的MainThreadUIOper /// </summary> /// <param name="str"></param> public delegate void UIOperDelegate(string str); /// <summary> /// 3、定義子線程執行操作函數 /// </summary> /// <param name="strMsg"></param> private void DoWork(object para) { string strPara = para.ToString(); for (int i = 0; i < 10; i++) { //注意BeginInvoke和Invoke的區別,根據實際情況選用,前者為異步,后者為同步 //也可以將new UIOperDelegate(MainThreadUIOper)提出來單獨定義 UIOperDelegate temp = new UIOperDelegate(MainThreadUIOper); 后面公共引用 this.Invoke(new UIOperDelegate(MainThreadUIOper), new object[] { DateTime.Now.ToString() }); //暫停1s Thread.Sleep(1000); } //完成后發送相關信息 this.BeginInvoke(new UIOperDelegate(MainThreadUIOper), new Object[] { "測試完成!" }); //使用 MessageBoxOptions.ServiceNotification是的窗口始終在最上一層顯示 MessageBox.Show("提示", "測試完成!", MessageBoxButtons.OK, MessageBoxIcon.None, MessageBoxDefaultButton.Button1, MessageBoxOptions.ServiceNotification); } /// <summary> /// 4、在主線程中開啟子線程並傳遞參數給子線程 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btn_start_Click(object sender, EventArgs e) { memoEdit_main.Text = ""; memoEdit_main.Update(); //注意,如果需要給子線程傳遞參數,需要使用ParameterizedThreadStart,否則使用ThreadStart即可,后面直接thread.Start(); Thread thread = new Thread(new ParameterizedThreadStart(DoWork)); thread.Start("para"); } #endregion #region 使用BackgroundWorker方法進行異步操作 /// <summary> /// 1、定義BackgroundWorker對象 /// </summary> private BackgroundWorker m_backgroundWorker = null; /// <summary> /// 2、主體方法,定義子操作函數 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> public void m_backgroundWorker_DoWork(object sender, DoWorkEventArgs e) { for (int i = 0; i < 10; i++) { //在線程中更新UI(通過ReportProgress方法) m_backgroundWorker.ReportProgress(50, DateTime.Now.ToString()); //暫停1s Thread.Sleep(1000); } //完成后發送相關信息 m_backgroundWorker.ReportProgress(100, "操作完成"); } /// <summary> /// 3、定義執行UI更新事件 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> public void m_backgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e) { memoEdit_main.Text = memoEdit_main.Text + "\r\n" + e.UserState.ToString(); memoEdit_main.SelectionStart = memoEdit_main.Text.Length; memoEdit_main.ScrollToCaret(); Application.DoEvents(); } /// <summary> /// 4、注冊事件(執行線程主體、執行UI更新事件) /// 啟動子線程 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btn_prop_Click(object sender, EventArgs e) { m_backgroundWorker = new System.ComponentModel.BackgroundWorker(); //設置報告進度更新,注意此選項必須設置為true,否則無法返回進度 m_backgroundWorker.WorkerReportsProgress = true; //注冊線程主體方法 m_backgroundWorker.DoWork += new DoWorkEventHandler(m_backgroundWorker_DoWork); //注冊更新UI方法 m_backgroundWorker.ProgressChanged += new ProgressChangedEventHandler(m_backgroundWorker_ProgressChanged); //注冊子線程執行完成事件 m_backgroundWorker.RunWorkerCompleted += new System.ComponentModel.RunWorkerCompletedEventHandler(this.m_backgroundWorker_RunWorkerCompleted); this.m_backgroundWorker.RunWorkerAsync(); } /// <summary> /// 子線程執行完成后操作 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> public void m_backgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { MessageBox.Show("操作完成!"); } #endregion #region 通過UI線程的SynchronizationContext的Post/Send方法更新UI /// <summary> /// 1、定義UI線程的同步上下文 /// </summary> SynchronizationContext m_SyncContext = null; /// <summary> /// 3、定義線程的主體方法 /// </summary> private void ThreadProcSafePost() { for (int i = 0; i < 10; i++) { //在線程中更新UI(通過UI線程同步上下文m_SyncContext) m_SyncContext.Post(SetTextSafePost, DateTime.Now.ToString()); //暫停1s Thread.Sleep(1000); } //完成后發送相關信息 m_SyncContext.Post(SetTextSafePost, "操作完成"); } /// <summary> /// 4、更新UI方法 /// </summary> /// <param name="text"></param> private void SetTextSafePost(object str) { memoEdit_main.Text = memoEdit_main.Text + "\r\n" + str.ToString(); memoEdit_main.SelectionStart = memoEdit_main.Text.Length; memoEdit_main.ScrollToCaret(); Application.DoEvents(); } /// <summary> /// 2、獲取UI線程同步上下文(建議在窗體構造函數或FormLoad事件中,這里因為測試方便,直接放在了這里) /// 啟動線程 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void btn_sync_Click(object sender, EventArgs e) { //獲取UI線程同步上下文 m_SyncContext = SynchronizationContext.Current; Thread thread = new Thread(new ThreadStart(this.ThreadProcSafePost)); thread.Start(); } #endregion } }
參考:
https://www.cnblogs.com/marshal-m/p/3201051.html
https://www.cnblogs.com/mq0036/p/3678440.html