加载中…
个人资料
  • 博客等级:
  • 博客积分:
  • 博客访问:
  • 关注人气:
  • 获赠金笔:0支
  • 赠出金笔:0支
  • 荣誉徽章:
正文 字体大小:

C#Winform跨线程访问UI控件

(2019-11-01 09:22:30)
标签:

cwinform

跨线程访问ui控件

分类: 代码天地
本来想写下C# winform 在线程中更新UI控件的问题,看到网上有这么多的介绍,就转来3篇了。

第一篇  转自

C#中禁止跨线程直接访问控件,InvokeRequired是为了解决这个问题而产生的,当一个控件的InvokeRequired属性值为真时,说明有一个创建它以外的线程想访问它。此时它将会在内部调用new MethodInvoker(LoadGlobalImage)来完成下面的步骤,这个做法保证了控件的安全,你可以这样理解,有人想找你借钱,他可以直接在你的钱包中拿,这样太不安全,因此必须让别人先要告诉你,你再从自己的钱包把钱拿出来借给别人,这样就安全了

 ----------------------------------------------------------------------

 在设计中为了让界面与逻辑分离,我的做法是使用事件,界面只要响应事件来处理界面的显示就行了。而事件在逻辑处理中可能由不同的线程引发,这些事件的响应方法在修改界面中的控件内容时便会引发一个异常。

 这时就用到了Control.InvokeRequired 属性 与Invoke方法。

------------------------------------------------------------------------

获取一个值,该值指示调用方在对控件进行方法调用时是否必须调用 Invoke 方法,因为调用方位于创建控件所在的线程以外的线程中。 

如果控件的 Handle 是在与调用线程不同的线程上创建的(说明您必须通过 Invoke 方法对控件进行调用),则为 true;否则为 false。

Windows 窗体中的控件被绑定到特定的线程,不具备线程安全性 。因此,如果从另一个线程调用控件的方法,那么必须使用控件的一个 Invoke 方法来将调用封送到适当的线程。该属性可用于确定是否必须调用 Invoke 方法,当不知道什么线程拥有控件时这很有用。

 private   delegate   void  InvokeCallback( string  msg);
void  m_comm_MessageEvent( string  msg)
    {
     if (txtMessage.InvokeRequired)
     {
     InvokeCallback msgCallback  =   new  InvokeCallback(m_comm_MessageEvent);
     txtMessage.Invoke(msgCallback,  new   object []  { msg } );
    } 
     else 
     {
     txtMessage.Text  =  msg;
    } 
   }

说明:这个函数就是事件处理函数,txtMessage是一个文本框。

这样就做到了窗体中控件的线程安全性。

------------------------------------------------------------------------

InvokeRequired 当前线程不是创建控件的线程时为true

比如你可以自己开一个Thread,或使用Timer的事件来访问窗体上的控件的时候,在线程中窗体的这个属性就是True的。

 简单的说,如果有两个线程,Thread A和Thread B,并且有一个Control c,是在Thread A里面new的。

那么在Thread A里面运行的任何方法调用c.InvokeRequired都会返回false。

相反,如果在Thread B里面运行的任何方法调用c.InvokeRequired都会返回true。

是否是UI线程与结果无关。(通常Control所在的线程是UI线程,但是可以有例外)

也可以认为,在new Control()的时候,control用一个变量记录下了当前线程,在调用InvokeRequired时,返回当前线程是否不等于new的时候记录下来的那个线程。


=======------------
第二篇  转自

c#中多线程访问winform控件的若干问题小结

 更新时间:2013年10月31日 22:13:27   作者:   https://www.jb51.net/skin/2018/images/text-message.png 我要评论
大部分情况下都会碰到使用多线程控制界面上控件信息的问题。然而我们并不能用传统方法来解决这个问题,下面我将详细的介绍

我们在做winform应用的时候,大部分情况下都会碰到使用多线程控制界面上控件信息的问题。然而我们并不能用传统方法来解决这个问题,下面我将详细的介绍。

首先来看传统方法:

 

复制代码代码如下:

public partial class Form1 : Form
     {
        public Form1()
        {
            InitializeComponent();
        }

 

        private void Form1_Load(object sender, EventArgs e)
        {
            Thread thread = new Thread(ThreadFuntion);
            thread.IsBackground = true;
            thread.Start();
        }
        private void ThreadFuntion()
        {
            while (true)
            {
                this.textBox1.Text = DateTime.Now.ToString();
                Thread.Sleep(1000);
            }
        }
    }

 

运行这段代码,我们会看到系统抛出一个异常:Cross-thread operation not valid:Control 'textBox1' accessed from  a thread other than the thread it was created on . 这是因为.net 2.0以后加强了安全机制,不允许在winform中直接跨线程访问控件的属性。那么怎么解决这个问题呢,下面提供几种方案。

      第一种方案,我们在Form1_Load()方法中加一句代码:

 

复制代码代码如下:

private void Form1_Load(object sender, EventArgs e)
      {
            Control.CheckForIllegalCrossThreadCalls = false;
            Thread thread = new Thread(ThreadFuntion);
            thread.IsBackground = true;
            thread.Start();
      }

 

加入这句代码以后发现程序可以正常运行了。这句代码就是说在这个类中我们不检查跨线程的调用是否合法(如果没有 加这句话运行也没有异常,那么说明系统以及默认的采用了不检查的方式)。然而,这种方法不可取。我们查看CheckForIllegalCrossThreadCalls 这个属性的定义,就会发现它是一个static的,也就是说无论我们在项目的什么地方修改了这个值,他就会在全局起作用。而且像这种跨线程访问是否存在异常,我们通常都会去检查。如果项目中其他人修改了这个属性,那么我们的方案就失败了,我们要采取另外的方案。

下面来看第二种方案,就是使用delegate和invoke来从其他线程中控制控件信息。网上有很多人写了这种控制方式,然而我看了很多这种帖子,表明上看来是没有什么问题的,但是实际上并没有解决这个问题,首先来看网络上的那种不完善的方式:

 

复制代码代码如下:

public partial class Form1 : Form
    {
        private delegate void FlushClient();//代理
        public Form1()
        {
            InitializeComponent();

        }

        private void Form1_Load(object sender, EventArgs e)
        {

            Thread thread = new Thread(CrossThreadFlush);
            thread.IsBackground=true;
            thread.Start();
        }


        private void CrossThreadFlush()
        {
            //将代理绑定到方法 
            FlushClient fc = new FlushClient(ThreadFuntion);
            this.BeginInvoke(fc);//调用代理
        }

        private void ThreadFuntion()
        {
            while (true)
            {
                this.textBox1.Text = DateTime.Now.ToString();
                Thread.Sleep(1000);
            }
        }
    }

 

使用这种方式我们可以看到跨线程访问的异常没有了。但是新问题出现了,界面没有响应了。为什么会出现这个问题, 我们只是让新开的线程无限循环刷新,理论上应该不会对主线程产生影响的。其实不然,这种方式其实相当于把这个新开的线程“注入”到了主控制线程中,它取得了主线程的控制。只要这个线程不返回,那么主线程将永远都无法响应。就算新开的线程中不使用无限循环,使可以返回了。这种方式的使用多线程也失去了它本来的意义。

现在来让我们看看推荐的解决方案:

 

复制代码代码如下:

public partial class Form1 : Form
    {
        private delegate void FlushClient();//代理
        public Form1()
        {
            InitializeComponent();
        }

 

        private void Form1_Load(object sender, EventArgs e)
        {
            Thread thread = new Thread(CrossThreadFlush);
            thread.IsBackground = true;
            thread.Start();
        }


        private void CrossThreadFlush()
        {
            while (true)
            {
                //将sleep和无限循环放在等待异步的外面
                Thread.Sleep(1000);
                ThreadFunction();
            }
        }

        private void ThreadFunction()
        {
            if (this.textBox1.InvokeRequired)//等待异步
            {
                FlushClient fc = new FlushClient(ThreadFunction);
                this.Invoke(fc);//通过代理调用刷新方法
            }
            else
            {
                this.textBox1.Text = DateTime.Now.ToString();
            }
        }
    }

 

运行上述代码,我们可以看到问题已经被解决了,通过等待异步,我们就不会总是持有主线程的控制,这样就可以再不发生跨线程调用异常的情况下完成多线程对winform多线程控件的控制了。

对于深山老林提出的问题,我最近找到了更优的解决方案,利用了delegate的异步调用,大家可以看看:

 

复制代码代码如下:

public partial class Form1 : Form
    {
        private delegate void FlushClient();//代理
        public Form1()
        {
            InitializeComponent();
        }

 

        private void Form1_Load(object sender, EventArgs e)
        {
            Thread thread = new Thread(CrossThreadFlush);
            thread.IsBackground = true;
            thread.Start();
        }


        private void CrossThreadFlush()
        {
             FlushClient fc=new FlushClient(ThreadFunction);
             fc.BeginInvoke(null,null);
        }

        private void ThreadFunction()
        {
              while (true)
            {
                this.textBox1.Text = DateTime.Now.ToString();
                Thread.Sleep(1000);
            }
        }
    }

 

这种方法也可以直接简化为(因为delegate的异步就是开了一个异步线程):

 

复制代码代码如下:

public partial class Form1 : Form
    {
        private delegate void FlushClient();//代理
        public Form1()
        {
            InitializeComponent();
        }

 

        private void Form1_Load(object sender, EventArgs e)
        {
             FlushClient fc=new FlushClient(ThreadFunction); 
             fc.BeginInvoke(null,null);
        }

         private void ThreadFunction()
        {
              while (true)
            {

                this.textBox1.Text = DateTime.Now.ToString();
                Thread.Sleep(1000);
            }
        }
    }

 

贫僧用飘柔反馈

while (true)和Thread.Sleep(1000);不要放在invoke里面去控制就行了



======-------------
第三篇  转自


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事件中)
        /// 
        /// UI线程的同步上下文
        /// 
        SynchronizationContext m_SyncContext = null;
        public Form1()
        {
            InitializeComponent();
            //获取UI线程同步上下文
            m_SyncContext = SynchronizationContext.Current;
            //Control.CheckForIllegalCrossThreadCalls = false;
        }
        //第二步:定义线程的主体方法
        /// 
        /// 线程的主体方法
        /// 
        private void ThreadProcSafePost()
        {
            //...执行线程任务

            //在线程中更新UI(通过UI线程同步上下文m_SyncContext)
            m_SyncContext.Post(SetTextSafePost, "This text was set safely by SynchronizationContext-Post.");

            //...执行线程其他任务
        }
        //第三步:定义更新UI控件的方法
        /// 
        /// 更新文本框内容的方法
        /// 
        /// 
        private void SetTextSafePost(object text)
        {
            this.textBox1.Text = text.ToString();
        }
        //之后,启动线程
        /// 
        /// 启动线程按钮事件
        /// 
        /// 
        /// 
        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);

        //第二步:定义线程的主体方法
        /// 
        /// 线程的主体方法
        /// 
        private void ThreadProcSafe()
        {
            //...执行线程任务

            //在线程中更新UI(通过控件的.Invoke方法)
            this.SetText("This text was set safely.");

            //...执行线程其他任务
        }
        //第三步:定义更新UI控件的方法
        /// 
        /// 更新文本框内容的方法
        /// 
        /// 
        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;
            }
        }
        //之后,启动线程
        /// 
        /// 启动线程按钮事件
        /// 
        /// 
        /// 
        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)总结



0

阅读 收藏 喜欢 打印举报/Report
  

新浪BLOG意见反馈留言板 欢迎批评指正

新浪简介 | About Sina | 广告服务 | 联系我们 | 招聘信息 | 网站律师 | SINA English | 产品答疑

新浪公司 版权所有