概述

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

3. 通过BackgroundWorker取代Thread执行异步操作;
4. 通过设置窗体属性,取消线程安全检查来避免"跨线程操作异常"(非线程安全,建议不使用)。
下文中对以上3种方法应用进行举例说明,希望能对初识C# Winform的同学们有些帮助。

成文表分享交流之意,惶恐水平有限,文中理解和表述有错误之处还请大家多被批评指正。

更新记录:

2018年2月3日,根据网友评论提示更新错别字,BegainInvoke=》BeginInvoke。

正文

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(通过UI线程同步上下文m_SyncContext)
m_SyncContext.Post(SetTextSafePost, "This text was set safely by SynchronizationContext-Post.");
可以这样解读这行代码:向UI线程的同步上下文(m_SyncContext)中提交一个异步消息(UI线程,你收到消息后以异步的方式执行委托,调用方法SetTextSafePost,参数是“this text was ....”).

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#》

C# Winform 跨线程更新UI控件常用方法汇总(多线程访问UI控件)的更多相关文章

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

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

  2. C# Winform 跨线程更新UI控件常用方法汇总

    https://www.cnblogs.com/marshal-m/p/3201051.html

  3. Winform之跨线程更新UI

    Winform之跨线程更新UI 使用`Invoke`或者`BeginInvoke`与UI线程交互示例 参考及源码 使用Invoke或者BeginInvoke与UI线程交互示例 private void ...

  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. [WinForm]WinForm跨线程UI操作常用控件类大全

    前言 在C#开发的WinForm窗体程序开发的时候,经常会使用多线程处理一些比较耗时之类的操作.不过会有一个问题:就是涉及到跨线程操作UI元素. 相信才开始接触的人一定会遇上这个问题. 为了解决这个问 ...

  6. Winfrom 如何安全简单的跨线程更新控件

    来源:http://www.cnblogs.com/rainbowzc/archive/2010/09/29/1838788.html 由于多线程可能导致对控件访问的不一致,导致出现问题.C#中默认是 ...

  7. C# WinForm 跨线程访问控件(实用简洁写法)

    C# WinForm 跨线程访问控件(实用简洁写法) 1.<C# WinForm 跨线程访问控件(实用简洁写法)>       2.<基于.NET环境,C#语言 实现 TCP NAT ...

  8. 简短总结一下C#里跨线程更新UI(转)

    摘自: http://my.oschina.net/sdqxcxh/blog/53707 跨线程更新UI是写多线程程序尤其是通信类的程序经常遇到的问题,这里面主要的问题是冲突,比如数据线程想要更新UI ...

  9. 简短总结一下C#里跨线程更新UI

    摘自: http://my.oschina.net/sdqxcxh/blog/53707 跨线程更新UI是写多线程程序尤其是通信类的程序经常遇到的问题,这里面主要的问题是冲突,比如数据线程想要更新UI ...

随机推荐

  1. oracle 和c3p0 数据库的Time_Wait 过多问题的一个解决方案。

    项目是B/S模式,放在linux服务器上,tomcat和oracle11g在一台服务器上,tomcat读取数据库采用C3P0连接池,一直比较稳定,所以也没有去管.后来把tomcat放在一台win200 ...

  2. ajax文件下载

    今天想做一个csv导出,想做ajax下载 JQuery的ajax函数的返回类型只有xml.text.json.html等类型,没有“流”类型,所以要实现ajax下载,不能够使用相应的ajax函数进行文 ...

  3. HDU 5428 The Factor

    话说这题意真的是好难懂啊,尽管搜到了中文题意,然而还是没懂,最后看到了一个题解才懂的.http://www.cnblogs.com/Apro/p/4784808.html#3470972 题意:给出n ...

  4. √GMAP.NET 地图

    深入理解最强桌面地图控件GMAP.NET ---[更新]百度地图 enjoyeclipse 2013-11-18 22:23 阅读:3897 评论:20     深入理解最强桌面地图控件GMAP.NE ...

  5. 【dapper】.net平台下的框架

    http://www.cnblogs.com/yipu/archive/2012/11/21/2780199.html Method Duration Remarks Hand coded (usin ...

  6. android中相关的图形类

    Bitmap - 称作位图,一般位图的文件格式后缀为bmp,当然编码器也有很多如RGB565.RGB888.作为一种逐像素的显示对象执行效率高,但是缺点也很明显存储效率低.我们理解为一种存储对象比较好 ...

  7. git:fatal the current branch master has no upstream branch

    git push的时候发生上述错误. git push -u "resp" master resp为git仓库的“地址” reference中由解决方案 http://blog.c ...

  8. GTW likes math(BC 1001)

    GTW likes math Accepts: 472 Submissions: 2140 Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 1 ...

  9. inotifywait实现目录监控

    http://blog.csdn.net/firefoxbug/article/details/8188804

  10. new Date()的参数

    前两天发现手机页面的倒计时在Android上正常显示,在iPhone却不能显示. 后来又发现在ff和ie里也不显示.(以前只在chrome里看过,显示正常). 后来同事改了new Date()里字符串 ...