zoukankan      html  css  js  c++  java
  • [转] c#多线程(UI线程,控件显示更新) Invoke和BeginInvoke 区别

    如果只是直接使用子线程访问UI控件,直接看内容三,如果想深入了解从内容一看起。

    一、Control.Invoke和BeginInvoke方法的区别

    先上总结:

    Control.Invoke 方法 (Delegate) :拥有此控件的基础窗口句柄的线程上执行指定的委托。但委托的内容在UI线程上执行。

    Control.BeginInvoke 方法 (Delegate) :在创建控件的基础句柄所在线程上异步执行指定委托。但委托的内容在UI线程上执行。

    (一)Control的Invoke和BeginInvoke
    我们要基于以下认识:
    (1)Control的Invoke和BeginInvoke与Delegate的Invoke和BeginInvoke是不同的。
    (2)Control的Invoke和BeginInvoke的参数为delegate,委托的方法是在Control的线程上执行的,也就是我们平时所说的UI线程。

    我们以代码(一)来看(Control的Invoke)
    private delegate void InvokeDelegate();
    private void InvokeMethod(){
       //C代码段
    }
    private void butInvoke_Click(object sender, EventArgs e) {
       //A代码段.......
       this.Invoke(new InvokeDelegate(InvokeMethod));
       //B代码段......
    }
    你觉得代码的执行顺序是什么呢?记好Control的Invoke和BeginInvoke都执行在主线程即UI线程上
    A------>C---------------->B
    解释:(1)A在UI线程上执行完后,开始Invoke,Invoke是同步
    (2)代码段B并不执行,而是立即在UI线程上执行InvokeMethod方法,即代码段C。
    (3)InvokeMethod方法执行完后,代码段C才在UI线程上继续执行。

    看看代码(二),Control的BeginInvoke
    private delegate void BeginInvokeDelegate();
    private void BeginInvokeMethod(){
       //C代码段
    }
    private void butBeginInvoke_Click(object sender, EventArgs e) {
       //A代码段.......
       this.BeginInvoke(new BeginInvokeDelegate(BeginInvokeMethod));
       //B代码段......
    }
    你觉得代码的执行顺序是什么呢?记好Control的Invoke和BeginInvoke都执行在主线程即UI线程上
    A----------->B--------------->C慎重,这个只做参考。。。。。,我也不肯定执行顺序,如果有哪位达人知道的话请告知。
    解释::(1)A在UI线程上执行完后,开始BeginInvoke,BeginInvoke是异步
    (2)InvokeMethod方法,即代码段C不会执行,而是立即在UI线程上执行代码段B。
    (3)代码段B执行完后(就是说butBeginInvoke_Click方法执行完后),InvokeMethod方法,即代码段C才在UI线程上继续执行。

    由此,我们知道:
    Control的Invoke和BeginInvoke的委托方法是在主线程,即UI线程上执行的。也就是说如果你的委托方法用来取花费时间长的数据,然后更新界面什么的,千万别在UI线程上调用Control.Invoke和Control.BeginInvoke,因为这些是依然阻塞UI线程的,造成界面的假死。

    那么,这个异步到底是什么意思呢?

    异步是指相对于调用BeginInvoke的线程异步,而不是相对于UI线程异步,你在UI线程上调用BeginInvoke ,当然不行了。----摘自"Invoke和BeginInvoke的真正涵义"一文中的评论。
    BeginInvoke的原理是将调用的方法Marshal成消息,然后调用Win32 API中的RegisterWindowMessage()向UI窗口发送消息。----摘自"Invoke和BeginInvoke的真正涵义"一文中的评论。

    (二)我们用Thread来调用BeginInvoke和Invoke
          我们开一个线程,让线程执行一些耗费时间的操作,然后再用Control.Invoke和Control.BeginInvoke回到用户UI线程,执行界面更新。

    代码(三)  Thread调用Control的Invoke
    private Thread invokeThread;        
    private delegate void invokeDelegate();   
    private void StartMethod(){
       //C代码段......
       Control.Invoke(new invokeDelegate(invokeMethod)); 

    【上面这句话作用是把消息发送给UI线程,然后在这个点阻塞,等UI线程处理完发送的消息后才继续往下执行,特别适合阻塞更新UI控件】

      //D代码段......
    }
    private void invokeMethod(){
      //E代码段
    }
    private void butInvoke_Click(object sender, EventArgs e) {
       //A代码段.......
       invokeThread = new Thread(new ThreadStart(StartMethod));

       invokeThread.Start();

       //B代码段......
    }
    你觉得代码的执行顺序是什么呢?记好Control的Invoke和BeginInvoke都执行在主线程即UI线程上
    A------>(Start一开始B和StartMethod的C就同时执行)---->(C执行完了,不管B有没有执行完,invokeThread把消息封送(invoke)给UI线程,然后自己等待)---->UI线程处理完butInvoke_Click消息后,处理invokeThread封送过来的消息,执行invokeMethod方法,即代码段E,处理往后UI线程切换到invokeThread线程。
    这个Control.Invoke是相对于invokeThread线程同步的,阻止了其运行。

    解释:
    1。UI执行A
    2。UI开线程InvokeThread,B和C同时执行,B执行在线程UI上,C执行在线程invokeThread上。
    3。invokeThread封送消息给UI,然后自己等待,UI处理完消息后,处理invokeThread封送的消息,即代码段E
    4。UI执行完E后,转到线程invokeThread上,invokeThread线程执行代码段D

    代码(四)  Thread调用Control的BeginInvoke
    private Thread beginInvokeThread;
    private delegate void beginInvokeDelegate();
    private void StartMethod(){
       //C代码段......
       Control.BeginInvoke(new beginInvokeDelegate(beginInvokeMethod));

    【上面这句话作用是把消息发送给UI线程,然后不阻塞继续往下执行D代码,也就是不再管发送的消息了,UI线程处理完自己的事情后会处理这个发送的消息,一般很少用这种方法更新UI控件,因为一般情况下都要求阻塞次(子)线程更新的UI控件的,只要不阻塞UI主(父)线程,让用户感觉不到界面死机就行。】

      //D代码段......
    }
    private void beginInvokeMethod(){
      //E代码段
    }
    private void butBeginInvoke_Click(object sender, EventArgs e) {
       //A代码段.......
       beginInvokeThread = new Thread(new ThreadStart(StartMethod));
       beginInvokeThread .Start();
       //B代码段......
    }
    你觉得代码的执行顺序是什么呢?记好Control的Invoke和BeginInvoke都执行在主线程即UI线程上
    A在UI线程上执行----->beginInvokeThread线程开始执行,UI继续执行代码段B,并发地invokeThread执行代码段C-------------->不管UI有没有执行完代码段B,这时beginInvokeThread线程把消息封送给UI,单自己并不等待,继续向下执行-------->UI处理完butBeginInvoke_Click消息后,处理beginInvokeThread线程封送过来的消息。


    解释:
    1。UI执行A
    2。UI开线程beginInvokeThread,B和C同时执行,B执行在线程UI上,C执行在线程beginInvokeThread上。
    3。beginInvokeThread封送消息给UI,然后自己继续执行代码D,UI处理完消息后,处理invokeThread封送的消息,即代码段E
    有点疑问:如果UI先执行完毕,是不是有可能过了段时间beginInvokeThread才把消息封送给UI,然后UI才继续执行封送的消息E。如图浅绿的部分。


    Control的BeginInvoke是相对于调用它的线程,即beginInvokeThread相对是异步的。
    因此,我们可以想到。如果要异步取耗费长时间的数据,比如从数据库中读大量数据,我们应该这么做。
    (1)如果你想阻止调用线程,那么调用代码(三),代码段D删掉,C改为耗费长时间的操作,因为这个操作是在另外一个线程中做的。代码段E改为更新界面的方法。
    (2)如果你不想阻止调用线程,那么调用代码(四),代码段D删掉,C改为耗费长时间的操作,因为这个操作是在另外一个线程中做的。代码段E改为更新界面的方法。

    文章转自:http://www.cnblogs.com/mashang/archive/2009/08/01/1536730.html

    二、有了一点上面的知识,来看“C#多线程异步访问winform中控件(为了加深对上面的理解,但还不是最简单的方法,最简单的方法见下面)”:

    转自http://blog.csdn.net/ajrm0925/article/details/5314099

    我们在做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);//调用代理 【注意this只Form窗体,所以也是一个Control,具有BeginInvoke方法,并且在UI线程中执行代理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);//通过代理调用刷新方法 【注意this只Form窗体,所以也是一个Control,具有BeginInvoke方法,并且在UI线程中执行代理fc】
                }
                else
                {
                    this.textBox1.Text = DateTime.Now.ToString();
                }
            }
        }

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

    三、最简单的C#多线程异步访问winform中控件的方法,其实就是对上面“推荐的方案进行总结和简化的来的”

    转自:http://blog.csdn.net/ajrm0925/article/details/5314195

    private void button1_Click(object sender, EventArgs e)
    {
        ThreadStart ts = new ThreadStart(add);
        Thread th = new Thread(ts);
        th.Start(); 【启动子线程add】
     }

    delegate void DelChangText(string ss);  //【先声明一个代理,是一个类型】

    void add()  【这是在子线程中执行的内容】
    {
       int a = 1;
       int b = 2;
       string sum = Convert.ToString(a + b);
       // 计算完成需要在一个文本框里显示
       this.BeginInvoke(new DelChangText(intoText), sum);  //【从add子线程转到UI主线程执行intoText方法,这里也可以使用this.Invoke,至于区别见大标题一】

       //或改为intoText(sum);执行结果也正确
    }

    void intoText(string sum)                     //【再写对控件进行操作的方法,注意它的一般固定写法】
    {
       if (this.InvokeRequired)  //借助this即Form对象来判断正在执行的代码是不是在UI线程中,如果是在UI线程,则说明没在异步调用,所以条件为假
       {
          this.BeginInvoke(new DelChangText(intoText), sum);  //【this是Form控件,所以changText会在UI线程中执行,不懂的看大标题一和二】

       }
       else
       {
          textBox1.Text = sum;
       }
    }

     解释:从上面代码来看if (this.InvokeRequired)似乎是多余的因为子线程add中使用了this.BeginInvoke转到UI线程中执行inntoText方法,所以if (this.InvokeRequired)条件判断永远为假,但这样写安全,谁能保证子线程中开启的inntoText方法在UI线程中执行呢。

    其实可以把上面add方法中的this.BeginInvoke改为直接执行intoText试试看,是不是进入到if语句中了。此时执行也是正确的

  • 相关阅读:
    HDU 3951 (博弈) Coin Game
    HDU 3863 (博弈) No Gambling
    HDU 3544 (不平等博弈) Alice's Game
    POJ 3225 (线段树 区间更新) Help with Intervals
    POJ 2528 (线段树 离散化) Mayor's posters
    POJ 3468 (线段树 区间增减) A Simple Problem with Integers
    HDU 1698 (线段树 区间更新) Just a Hook
    POJ (线段树) Who Gets the Most Candies?
    POJ 2828 (线段树 单点更新) Buy Tickets
    HDU 2795 (线段树 单点更新) Billboard
  • 原文地址:https://www.cnblogs.com/firstdown/p/3730019.html
Copyright © 2011-2022 走看看