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

对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=116:47:44.924:BeginWorking_Invoke 线程ID=11
16:47:45.133:InvokeRequired=true 线程ID=1116:47:45.139:Sleep2秒 线程ID=116:47:47.144:InvokeRequired=false 线程ID=116:47:47.159:after Invoke 线程ID=11
16:47:47.363:InvokeRequired=true 线程ID=1116:47:47.371:Sleep2秒 线程ID=116:47:49.392:InvokeRequired=false 线程ID=116:47:49.407:after Invoke 线程ID=11
16:47:49.622:InvokeRequired=true 线程ID=1116:47:49.628:Sleep2秒 线程ID=116:47:51.638:InvokeRequired=false 线程ID=116:47:51.642:after Invoke 线程ID=11
16:47:51.857:InvokeRequired=true 线程ID=1116:47:51.863:Sleep2秒 线程ID=116:47:53.880:InvokeRequired=false 线程ID=116:47:53.888:after Invoke 线程ID=11
16:47:54.099:InvokeRequired=true 线程ID=1116:47:54.104:Sleep2秒 线程ID=116:47:56.118:InvokeRequired=false 线程ID=116: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=1backgroundWorker1_DoWork 线程ID=4inputArgument=hello world
backgroundWorker1_ProgressChanged 线程ID=1backgroundWorker1_ProgressChanged 线程ID=1backgroundWorker1_ProgressChanged 线程ID=1backgroundWorker1_ProgressChanged 线程ID=1backgroundWorker1_ProgressChanged 线程ID=1backgroundWorker1_ProgressChanged 线程ID=1backgroundWorker1_ProgressChanged 线程ID=1backgroundWorker1_ProgressChanged 线程ID=1backgroundWorker1_ProgressChanged 线程ID=1backgroundWorker1_ProgressChanged 线程ID=1backgroundWorker1_RunWorkerCompleted 线程ID=1

通过日志同样可以看出:

  1. 其中DoWork事件的处理程序(本例的backgroundWorker1_DoWork)用来执行耗时的业务操作,该部分代码由后台线程执行,而非UI线程,也正因此,backgroundWorker1_DoWork代码中就无法操作控件资源。

  2. BackgroundWorker的其它几个事件处理程序,如backgroundWorker1_ProgressChanged、backgroundWorker1_RunWorkerCompleted,就都由UI线程来执行,因此也就可以直接操作控件资源。

补充1:BackgroundWorker的ReportProgress(int percentProgress, object? userState)重载方法,其中userState参数可以承载percentProgress之外的一些有用信息。在scanningWorker_ProgressChanged中通过e.UserState接收并。

补充2:虽然RunWorkerCompletedEventArgs类型定义了UserState属性,但是其值始终为null,因此在RunWorkerCompleted事件处理程序中需要用e.Result来传递"结果"数据。

通过源码可以看到UserState没有赋值。

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. C# Winform 跨线程更新UI控件常用方法汇总(多线程访问UI控件)

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

  2. c#在WinForm中重写ProgressBar控件(带%的显示)

    c#在WinForm中重写ProgressBar控件(带%的显示) 2009-05-14 13:13 #region 定义textProgressBar控件的类 namespace csPublish ...

  3. WinForm中使用Excel控件

     最近项目中要在WinForm中使用Excel控件,经过几天的研究,现在总结一下成果. 在WinForm中使用Excel控件主要有三种方法:WebBrowser.DSOFramer.OWC.下面分别 ...

  4. winform中使用ZedGraphControl控件做图

    winform中使用ZedGraphControl控件做图. 一些方法的总结 public class ChartEx : ZedGraph.ZedGraphControl{#region initp ...

  5. WinForm中关于DataGridView控件的一些应用

    转载于新浪 美林居士 的博客:            blog.sina.com.cn/s/blog_797a56d20101daiw.html4 在.NET4.0中,以表格形式存储的数据通常是Dat ...

  6. Winform中使用printDocument控件打印pictureBox中的二维码照片

    场景 Winform中使用zxing和Graphics实现自定义绘制二维码布局: https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/1 ...

  7. Winform中使用用户控件实现带行数和标尺的RichTextBox(附代码下载)

    场景 RichTextBox控件允许用户输入和编辑文本的同时提供了比普通的TextBox控件更高级的格式特征. 效果 注: 博客主页: https://blog.csdn.net/badao_lium ...

  8. QT开发之调用其它类中的UI控件的方法

    比如类A和类B, 在B中有一个QTreeWidget控件,需要在A中调用操作 先在A中声明一个 QTreeWidget *tree; 然后在B中声明一个类A的指针 A *a; a->tree = ...

  9. wpf 绑定数据无法更新ui控件可能存在的问题

    BindingMode的枚举值有: ① OneWay ② TwoWay ③ OneTime:根据源端属性值设置目标属性值,之后的改变会被忽略,除非调用BindingExpression.UpdateT ...

最新文章

  1. 性能测试---商场模型分析
  2. Node.js 体验-在Windows Azure工作者角色上托管Node.js
  3. hosts文件_电脑修改hosts文件屏蔽网站方法
  4. Runtime底层原理--Runtime简介、函数注释
  5. MySQL助手_java 8.0Mysql 助手类
  6. HTML 5 aside 标签
  7. 【零基础学Java】—this关键字的三种用法+Java继承的三个特点(二十一)
  8. 中国塑料食品和饮料包装行业市场供需与战略研究报告
  9. Charles使用手册
  10. jieba java_【NLP】【一】中文分词之jieba
  11. linux驱动 平台设备驱动模型
  12. idea继承后重新方法快捷键_idea 查看类继承关系的快捷键
  13. 2019腾讯广告算法大赛解析
  14. Python turtle制作书法作品——《鸟鸣涧》
  15. 浅尝 | 从 0 到 1 Vue 组件库封装
  16. 《黄帝内经.生气通天论篇》不生病的智慧节选01
  17. 创造性思维与创新方法-2019春-期末试题
  18. 嵌入式编程中volatile的重要性
  19. Kafka多租户(配额)管理
  20. 熊猫烧香李俊涉嫌开设赌场罪被再度批捕

热门文章

  1. android banner加载布局,Android知识点之图片轮播控件Banner
  2. 旧书网购_基于旧书的新工作簿
  3. 用Python输入玫瑰花语
  4. 小工具 MyTool
  5. Linux 监控工具 tsar
  6. 参与 DDoS 攻击的物联网设备类型分布
  7. 美泰推出首款获CarbonNeutral(R)认证的Matchbox(R) Tesla Roadster压铸模型车,使用9
  8. 记账本——UML建模
  9. linux操作系统启动流程与kickstart文件制作
  10. 网站关键字排名查询工具