C#子线程更新主线程控件方法汇总
背景描述:近期为现场编写了一个数据处理工具,因数据量较大,执行时间超过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
posted on 2021-02-25 16:02 jingkunliu 阅读(2960) 评论(0) 编辑 收藏 举报