• C# Winform 跨线程更新UI控件常用方法汇总 (转)


    概述

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

    MSDN, How to: Make Thread-Safe Calls to Windows Forms Controls

    事理,C#线程中安全访问控件(重用委托,避免繁复的delegate,Invoke)总结

     
     
  • 相关阅读:
    配置 Linux 服务器 SSH 安全访问的四个小技巧
    线性代数
    转载的其它人博客
    c#下dll引入错误的问题
    游戏中简单代码
    明年的任务
    c# 异步通信网络中存在的问题
    解决导入五万条数据缓慢问题
    c#中关于结构体和字节数组转化
    写给现在的自己
  • 原文地址:https://www.cnblogs.com/xihong2014/p/13934990.html
Copyright © 2020-2023  润新知