这是一个古老的话题。。。直入主题吧!

对winfrom的控件来说,多线程操作非常容易导致复杂且严重的bug,比如不同线程可能会因场景需要强制设置控件为不同的状态,进而引起并发、加锁、死锁、阻塞等问题。为了避免和解决上述可能出现的问题,微软要求必须是控件的创建线程才能操作控件资源,其它线程不允许直接操作控件。但是现代应用又不是单线程应用,无论如何肯定会存在其它线程需要更新控件的需求,于是微软两种方案来解决相关问题:InvokeRequired方案和BackgroundWorker方案。

演示程序效果图和源码

查看代码
using System.ComponentModel;
using System.Diagnostics;
using System.Timers;
using Tccc.DesktopApp.WinForms1.BLL; namespace Tccc.DesktopApp.WinForms1
{
public partial class UIUpdateDemoForm : Form
{
/// <summary>
///
/// </summary>
public UIUpdateDemoForm()
{
InitializeComponent();
backgroundWorker1.WorkerReportsProgress = true;
backgroundWorker1.WorkerSupportsCancellation = true;
}
#region 演示InvokeRequired
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void invokeRequiredBtn_Click(object sender, EventArgs e)
{
Debug.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff") + "invokeRequiredBtn_Click 线程ID=" + Thread.CurrentThread.ManagedThreadId); new Thread(() =>
{
Debug.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff") + "BeginWorking_Invoke 线程ID=" + Thread.CurrentThread.ManagedThreadId);
BLLWorker.BeginWorking_Invoke(this, "some input param");
}).Start(); } /// <summary>
///
/// </summary>
public void UpdatingProgress(int progress)
{
if (this.InvokeRequired)
{
Debug.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff") + "InvokeRequired=true 线程ID=" + Thread.CurrentThread.ManagedThreadId);
this.Invoke(new Action(() =>
{
Debug.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff") + "Sleep2秒 线程ID=" + Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(2000);//模拟UI操作慢
UpdatingProgress(progress);
}));
Debug.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff") + "after Invoke 线程ID=" + Thread.CurrentThread.ManagedThreadId);
}
else
{
Debug.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff") + "InvokeRequired=false 线程ID=" + Thread.CurrentThread.ManagedThreadId);
richTextBox1.Text += DateTime.Now.ToString("HH:mm:ss") + ":执行进度" + progress + "%" + Environment.NewLine;
}
}
#endregion #region 演示BackgroundWorker /// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void bgWorkerBtn_Click(object sender, EventArgs e)
{
new Thread(() =>
{
//Control.CheckForIllegalCrossThreadCalls = true;
//richTextBox1.Text = "可以了?";
}).Start(); Debug.WriteLine("bgWorkerBtn_Click 线程ID=" + Thread.CurrentThread.ManagedThreadId);
if (!backgroundWorker1.IsBusy)
{
richTextBox1.Text = String.Empty;
backgroundWorker1.RunWorkerAsync("hello world");//
}
}
private void bgWorkerCancelBtn_Click(object sender, EventArgs e)
{
Debug.WriteLine("bgWorkerCancelBtn_Click 线程ID=" + Thread.CurrentThread.ManagedThreadId);
if (backgroundWorker1.IsBusy)
{
backgroundWorker1.CancelAsync();//
}
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
Debug.WriteLine("backgroundWorker1_DoWork 线程ID=" + Thread.CurrentThread.ManagedThreadId); BLLWorker.BeginWorking(sender, e);//控件遍历传递到业务处理程序中 }
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
Debug.WriteLine("backgroundWorker1_ProgressChanged 线程ID=" + Thread.CurrentThread.ManagedThreadId);
richTextBox1.Text += Environment.NewLine + DateTime.Now.ToString("HH:mm:ss.fff") + ":执行进度" + e.ProgressPercentage + "%";
}
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
Debug.WriteLine("backgroundWorker1_RunWorkerCompleted 线程ID=" + Thread.CurrentThread.ManagedThreadId);
if (e.Cancelled)
{
richTextBox1.Text += Environment.NewLine + DateTime.Now.ToString("HH:mm:ss.fff") + ":已取消";
}
else if (e.Error != null)
{
richTextBox1.Text += Environment.NewLine + DateTime.Now.ToString("HH:mm:ss.fff") + ":发生错误:" + e.Error.Message;
}
else
{
richTextBox1.Text += Environment.NewLine + DateTime.Now.ToString("HH:mm:ss.fff") + ":执行完成";
richTextBox1.Text += Environment.NewLine + DateTime.Now.ToString("HH:mm:ss.fff") + ":执行结果=" + e.Result;
}
}
#endregion }
public class BLLWorker
{
/// <summary>
///
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public static void BeginWorking_Invoke(UIUpdateDemoForm form, string inputData)
{
int counter = 0;
int max = 5;
while (counter < max)
{
System.Threading.Thread.Sleep(200);
counter++;
form.UpdatingProgress(counter * 20);
}
}
/// <summary>
/// 模拟耗时操作(下载、批量操作等)
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public static void BeginWorking(object sender, DoWorkEventArgs e)
{
BackgroundWorker worker = sender as BackgroundWorker;
Debug.WriteLine("inputArgument=" + e.Argument as string); for (int i = 1; i <= 10; i++)
{
if (worker.CancellationPending == true)//检测是否被取消
{
e.Cancel = true;
break;
}
else
{
// Perform a time consuming operation and report progress.
System.Threading.Thread.Sleep(200);
worker.ReportProgress(i * 10);
}
}
e.Result = "result xxxx"; }
}
}

InvokeRequired方案

上述代码中,this.InvokeRequired属性就是用来判断当前线程和this控件的创建线程是否一致。

  • 当其值=false时,代表当前执行线程就是控件的创建线程,可以直接操作控件。
  • 当其值=true时,代表当前线程不是控件的创建线程,需要调用Invoke方法来实现操作控件。

问题来了,调用Invoke()怎么就能实现操作控件了呢?我们在演示程序中的UpdatingProgress()增加了详细的记录,调试输出如下:

16:47:44.907:invokeRequiredBtn_Click 线程ID=1
16:47:44.924:BeginWorking_Invoke 线程ID=11 16:47:45.133:InvokeRequired=true 线程ID=11
16:47:45.139:Sleep2秒 线程ID=1
16:47:47.144:InvokeRequired=false 线程ID=1
16:47:47.159:after Invoke 线程ID=11 16:47:47.363:InvokeRequired=true 线程ID=11
16:47:47.371:Sleep2秒 线程ID=1
16:47:49.392:InvokeRequired=false 线程ID=1
16:47:49.407:after Invoke 线程ID=11 16:47:49.622:InvokeRequired=true 线程ID=11
16:47:49.628:Sleep2秒 线程ID=1
16:47:51.638:InvokeRequired=false 线程ID=1
16:47:51.642:after Invoke 线程ID=11 16:47:51.857:InvokeRequired=true 线程ID=11
16:47:51.863:Sleep2秒 线程ID=1
16:47:53.880:InvokeRequired=false 线程ID=1
16:47:53.888:after Invoke 线程ID=11 16:47:54.099:InvokeRequired=true 线程ID=11
16:47:54.104:Sleep2秒 线程ID=1
16:47:56.118:InvokeRequired=false 线程ID=1
16:47:56.126:after Invoke 线程ID=11

结合程序与执行日志,可以得到以下结论:

  1. 首先,在Invoke()方法前是线程11在执行,Invoke()内的代码就变成线程1在执行了,说明此处发生了线程切换。这也是Invoke()的核心作用:切换到UI线程(1号)来执行Invoke()内部代码。
  2. after Invoke日志的线程ID=11,说明Invoke()执行结束后,还是由之前的线程继续执行后续代码。
  3. after Invoke操作的日志时间显示是1号线程睡眠2秒后执行的,说明Invoke()执行期间,其后的代码是被阻塞的。
  4. 最后,通过程序总耗时来看,由于操作控件都需要切换为UI线程来执行,因此UI线程执行的代码中一旦有耗时的操作(比如本例的Sleep),将直接阻塞后续其它的操作,同时伴随着客户端程序界面的响应卡顿现象。

BackgroundWorker方案

BackgroundWorker是一个隐形的控件,这是微软封装程度较高的方案,它使用事件驱动模型。

演示程序的日志输出为:

bgWorkerBtn_Click 线程ID=1
backgroundWorker1_DoWork 线程ID=4
inputArgument=hello world backgroundWorker1_ProgressChanged 线程ID=1
backgroundWorker1_ProgressChanged 线程ID=1
backgroundWorker1_ProgressChanged 线程ID=1
backgroundWorker1_ProgressChanged 线程ID=1
backgroundWorker1_ProgressChanged 线程ID=1
backgroundWorker1_ProgressChanged 线程ID=1
backgroundWorker1_ProgressChanged 线程ID=1
backgroundWorker1_ProgressChanged 线程ID=1
backgroundWorker1_ProgressChanged 线程ID=1
backgroundWorker1_ProgressChanged 线程ID=1
backgroundWorker1_RunWorkerCompleted 线程ID=1

通过日志同样可以看出:

  1. 其中DoWork事件的处理程序(本例的backgroundWorker1_DoWork)用来执行耗时的业务操作,该部分代码由后台线程执行,而非UI线程,也正因此,backgroundWorker1_DoWork代码中就无法操作控件资源。
  2. BackgroundWorker的其它几个事件处理程序,如backgroundWorker1_ProgressChanged、backgroundWorker1_RunWorkerCompleted,就都由UI线程来执行,因此也就可以直接操作控件资源。

Control.CheckForIllegalCrossThreadCalls是咋回事?

官方注释:

Gets or sets a value indicating whether to catch calls on the wrong thread
that access a control's System.Windows.Forms.Control.Handle property
when an application is being debugged.

When a thread other than the creating thread of a control tries to access one of that control's methods or properties, it often leads to unpredictable results. A common invalid thread activity is a call on the wrong thread that accesses the control's Handle property. Set CheckForIllegalCrossThreadCalls to true to find and diagnose this thread activity more easily while debugging.

通俗理解:

虽然微软不建议其它线程操作控件,但是如果就这么写了,程序也能执行,比如下面的情况:

private void invokeRequiredBtn_Click(object sender, EventArgs e)
{
Debug.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff") + ":invokeRequiredBtn_Click 线程ID=" + Thread.CurrentThread.ManagedThreadId);
new Thread(() =>
{
richTextBox1.Text = DateTime.Now.ToString();
}).Start();
}

而Control.CheckForIllegalCrossThreadCalls这个属性就是用来设置,是否完全禁止跨线程的操作控件。当设置true,上述操作就完全不能执行了。

注意:在VS中F5调试时,此值默认=true。报错效果:

双击生成的exe执行时,此值默认=false,程序还可以执行。当Control.CheckForIllegalCrossThreadCalls设置为true时,双击exe执行程序会异常退出:

建议:如果是新开发的程序,建议设置为true,可以及早的发现隐患问题,避免程序复杂后需要付出高昂的分析成本。

总结

以上是winform开发的基础中的基础,本文在系统的查阅微软文档的基础上,通过演示程序推测和验证相关的逻辑关系。

同时联想到:由于控件的更新都需要UI线程来执行,因此当遇到程序客户端程序响应卡顿/卡死的情况,通过dump分析UI线程的堆栈,应该可以有所发现。

winform中更新UI控件的方案介绍的更多相关文章

  1. 富客户端 wpf, Winform 多线程更新UI控件

    前言 在富客户端的app中,如果在主线程中运行一些长时间的任务,那么应用程序的UI就不能正常相应.因为主线程要负责消息循环,相应鼠标等事件还有展现UI. 因此我们可以开启一个线程来格外处理需要长时间的 ...

  2. winform中如何在多线程中更新UI控件--ListView实时显示执行信息

    1.在winform中,所有对UI的操作,都得回到UI线程(主线程)上来,才不会报错 线程间操作无效: 从不是创建控件的线程访问它. 2.在winform中,允许通过Control.invoke对控件 ...

  3. WinForm/Silverlight多线程编程中如何更新UI控件的值

    单线程的winfom程序中,设置一个控件的值是很easy的事情,直接 this.TextBox1.value = "Hello World!";就搞定了,但是如果在一个新线程中这么 ...

  4. (转).NET 4.5中使用Task.Run和Parallel.For()实现的C# Winform多线程任务及跨线程更新UI控件综合实例

    http://2sharings.com/2014/net-4-5-task-run-parallel-for-winform-cross-multiple-threads-update-ui-dem ...

  5. C# Winform 跨线程更新UI控件常用方法汇总(多线程访问UI控件)

    概述 C#Winform编程中,跨线程直接更新UI控件的做法是不正确的,会时常出现“线程间操作无效: 从不是创建控件的线程访问它”的异常.处理跨线程更新Winform UI控件常用的方法有4种:1. ...

  6. C# Winform 跨线程更新UI控件常用方法总结(转)

    出处:http://www.tuicool.com/articles/FNzURb 概述 C#Winform编程中,跨线程直接更新UI控件的做法是不正确的,会时常出现“线程间操作无效: 从不是创建控件 ...

  7. C#子线程更新UI控件的方法总结

    http://blog.csdn.net/jqncc/article/details/16342121 在winform C/S程序中经常会在子线程中更新控件的情况,桌面程序UI线程是主线程,当试图从 ...

  8. Winform中修改WebBrowser控件User-Agent的方法(已经测试成功)

    using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.W ...

  9. WPF中嵌入WinForm中的webbrowser控件

    原文:WPF中嵌入WinForm中的webbrowser控件 使用VS2008创建WPF应用程序,需使用webbrowser.从工具箱中添加WPF组件中的webbrowser发现其中有很多属性事件不能 ...

随机推荐

  1. cloudcompare备忘录(1)

    1.找点 然后直接在需要的位置上点就会出现这个点的信息了~! 2.想看一个三d的切面时候 先选中切的目标 点击小剪刀~ 点击鼠标左键四次来框选,然后点击鼠标右键确认 再点击这个按钮就切好了

  2. argc 和 argv

    转载请注明来源:https://www.cnblogs.com/hookjc/ 如果用C寫一般的命令列工具,常透過main函式的argc,argv來取得使用者所輸入的命令參數.int main(int ...

  3. 群聊(udp)

    import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import ...

  4. 区区牛马蹉跎 ,不要向生活低下头 Linux的账号和管理

    账户和管理权限 1.管理用户账号和组账号 2.管理目录和文件的属性 1.Linux基于用户身份对资源访问进行控制:用户账号(超级用户.普通用户.程序用户) 组账号(基本组.附加组).UID (用户标识 ...

  5. 学多少年才算“精通Java”?

    我从毕业做程序员就开始用 Java,到现在已经工作快 20 年了.减去我做手游用 C++.Lua 的几年,再减去后来转管理写代码少的时间,我真正写 Java 代码的时间至少也在 10 年以上. 如果你 ...

  6. HashSet源码学习

    一.介绍 1.HashSet 2.HashSet和HashMap的区别 1.相同点 HashSet 内部使用 HashMap 存储元素,对应的键值对的键为 Set 的存储元素,值为一个默认的 Obje ...

  7. kubeasz 部署高可用 kubernetes 集群

    文章目录 环境准备 配置模板机 配置hosts解析 配置ssh 免密钥登陆 kubeasz 部署服务准备 配置主机清单 部署集群 环境准备 IP HOSTNAME SYSTEM 192.168.131 ...

  8. Nginx兼容框架的pathinfo模式与URL重写

    几乎所有的框架(ThinkPHP,Zend Framework,CI,Yii,laravel等)都会使用URL重写或者pathinfo模式,使URL看起来更美观,比如可以隐藏掉入口文件,并且有利于搜索 ...

  9. Spring声明式事务的两种配置方式(注解/xml)

    application配置tx:annotation-driven 配置声明式事务tx:TransactionManager 声明式事务需要数据源所以需要配置DataSource 使用:在类或者方法上 ...

  10. ceph-deploy离线部署ceph集群及报错解决FAQ

    ceph-deploy部署ceph集群 环境介绍 主机名 ip地址 操作系统 角色 备注 ceph-node1 10.153.204.13 Centos7.6 mon.osd.mds.mgr.rgw. ...