Control类的Invoke 和 BeginInvoke

Posted hanguoshun

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Control类的Invoke 和 BeginInvoke相关的知识,希望对你有一定的参考价值。

在多线程编程中,我们经常要在工作线程中去更新界面显示,而在多线程中直接调用界面控件的方法是错误的做法。

虽然可以通过在窗体的加载事件中,将C#内置控件(Control)类的CheckForIllegalCrossThreadCalls属性设置为false,屏蔽掉C#编译器对跨线程调用的检查。

public frmMain()
 {
     InitializeComponent();
     System.Windows.Forms.Control.CheckForIllegalCrossThreadCalls =false;
 }

使用上述的方法虽然可以保证程序正常运行并实现应用的功能,但是在实际的软件开发中,做如此设置是不安全的(不符合.NET的安全规范),在产品软件的开发中,此类情况是不允许的。

因为另外一个线程操作windows窗体上的控件,就会和主线程产生竞争,造成不可预料的结果,甚至死锁。因此windows GUI编程有一个规则,就是只能通过创建控件的线程来操作控件的数据,否则就可能产生不可预料的结果。

因此,dotnet里面,为了方便地解决这些问题,Control类实现了ISynchronizeInvoke接口,提供了Invoke和BeginInvoke方法来提供让其它线程更新GUI界面控件的机制。

public interface ISynchronizeInvoke

{

        [HostProtection(SecurityAction.LinkDemand, Synchronization=true, ExternalThreading=true)]

        IAsyncResult BeginInvoke(Delegate method, object[] args);

        object EndInvoke(IAsyncResult result);

        object Invoke(Delegate method, object[] args);

        bool InvokeRequired { get; }

}

}

 

 

需要纠正一个误区,那就是Control类上的异步调用BeginInvoke并没有开辟新的线程完成委托任务,而是让界面控件的所属线程完成委托任务的。看来异步操作就是开辟新线程的说法不一定准确。

如果工作线程(或者说是线程外)操作windows窗体控件,正确的做法是将工作线程中涉及更新界面的代码封装为一个方法,使用Invoke或者BeginInvoke方法,通过一个委托把调用“封送”到控件所属的线程上执行。

 

参考以下代码:

public delegate void treeinvoke();
private void UpdateTreeView()
{
    MessageBox.Show(System.Threading.Thread.CurrentThread.Name);
}
private void button1_Click(object sender, System.EventArgs e)
{
    System.Threading.Thread.CurrentThread.Name = "UIThread";
    treeView1.Invoke(new treeinvoke(UpdateTreeView));
}

 

 

public delegate void treeinvoke();
private void UpdateTreeView()
{
    MessageBox.Show(System.Threading.Thread.CurrentThread.Name);
}
private void button1_Click(object sender, System.EventArgs e)
{
    System.Threading.Thread.CurrentThread.Name = "UIThread";
    treeView1.BeginInvoke(new treeinvoke(UpdateTreeView));
}

 

看看运行结果,弹出的对话框中都显示的是 UIThread,这说明Invoke、BeginInvoke 所调用的委托根本就是在 UI 线程中执行的。

 

 

我们再看看下面的代码:

public delegate void treeinvoke();
private void UpdateTreeView()
{
    MessageBox.Show(Thread.CurrentThread.Name);
}
private void button1_Click(object sender, System.EventArgs e)
{
    Thread.CurrentThread.Name = "UIThread";
    Thread th = new Thread(new ThreadStart(StartThread));
    th.Start();
}
private void StartThread()
{
    Thread.CurrentThread.Name = "Work Thread";
    treeView1.Invoke(new treeinvoke(UpdateTreeView));
}

 

public delegate void treeinvoke();
private void UpdateTreeView()
{
    MessageBox.Show(Thread.CurrentThread.Name);
}
private void button1_Click(object sender, System.EventArgs e)
{
    Thread.CurrentThread.Name = "UIThread";
    Thread th = new Thread(new ThreadStart(StartThread));
    th.Start();
}
private void StartThread()
{
    Thread.CurrentThread.Name = "Work Thread";
    treeView1.BeginInvoke(new treeinvoke(UpdateTreeView));
}

再看看运行结果,弹出的对话框中显示的还是 UIThread,这说明什么?这说明 Invoke、BeginInvoke 方法所调用的委托无论如何都是在 UI 线程中执行的。

所以不管是Control类的Invoke方法还是BeginInvoke方法所调用的委托都是在UI线程中执行的。

调用委托的Invoke和BeginInvoke时才会申请一个基于线程池的线程。

异步方法,同步方法

Invoke  同步调用,BeginInvoke 异步调用

我们以代码(一)来看(Control的Invoke)

        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   (慎重,这个只做参考。。。。。,我也不肯定执行顺序,如果有哪位达人知道的话请告知。)这句话是转载别人的一块儿转载过来的。

我个人认为这样的顺序是正确的,因为BeginInvoke只是将消息“封送”到主线程上就返回。但是现在主线程还在执行butBeginInvoke_Click这个事件(或者说消息),

只有执行完这个事件,才会从消息队列里取消息再去执行。

解释::

(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线程的,造成界面的假死。而应该是在工作线程上调用Control.Invoke和Control.BeginInvoke。

比如以下代码

        private delegate void InvokeDelegate();
        private void InvokeMethod()
        {
            //C代码段
            for (int i=0;i<1000000000;i++)
            {
                int j = i;
            }
        }
        private void butInvoke_Click(object sender, EventArgs e)
        {
            //A代码段.......
            this.Invoke(new InvokeDelegate(InvokeMethod));
            //B代码段......
        }

点击按钮,界面还是会卡住。

比如以下代码:

 

        private delegate void BeginInvokeDelegate();
        private void BeginInvokeMethod()
        {
            //C代码段
            for (int i = 0; i < 1000000000; i++)
            {
                int j = i;
            }
        }
        private void butBeginInvoke_Click(object sender, EventArgs e)
        {
            //A代码段.......
            this.BeginInvoke(new BeginInvokeDelegate(BeginInvokeMethod));
            //B代码段......
        }

 

点击按钮,界面还是会卡住。正确的应该是在工作线程上调用Control.Invoke和Control.BeginInvoke,如下代码:下边代码只是为了演示工作线程里调用Control.Invoke,界面不卡顿,但并不太合理。正常的我们应该让新开的工作线程执行一些耗费时间的操作,然后再用Control.Invoke和Control.BeginInvoke回到用户UI线程,执行界面更新。而不应该像示例中用循环反复调用Control.Invoke。正常情况下应该是满足一定条件时,才会调用Control.Invoke。比如说下载一个大文件,主界面显示进度条。新开一个后台工作线程来执行下载任务,当每当文件下载1%的时候,然后执行一下Control.Invoke让主界面显示的进度条加1。  最终用户UI线程执行界面更新的回调方法,按理说这个回调方法里不应该有太多操作和计算,它应该只是一个界面更新的代码如本例子中只有this.textBox1.Text = value.ToString();这一句代码

 

如下代码是工作线程上调用Control.Invoke

        //定义回调
        private delegate void setTextValueCallBack(int value);
        //声明回调
        private setTextValueCallBack setCallBack;

        private void button1_Click(object sender, EventArgs e)
        {
            //实例化回调
            setCallBack = new setTextValueCallBack(SetValue);
            //创建一个线程去执行这个方法:创建的线程默认是前台线程
            Thread thread = new Thread(new ThreadStart(Test));
            //Start方法标记这个线程就绪了,可以随时被执行,具体什么时候执行这个线程,由CPU决定
            //将线程设置为后台线程
            thread.IsBackground = true;
            thread.Start();
        }
        private void Test()
        {
            for (int i = 0; i < 10000; i++)
            {
                //使用回调
                this.Invoke(setCallBack, i);
            }
        }

        /// <summary>
        /// 定义回调使用的方法
        /// </summary>
        /// <param name="value"></param>
        private void SetValue(int value)
        {
            this.textBox1.Text = value.ToString();
        }

点击按钮,界面不会住,可以任意拖动。

 

如下代码是工作线程上调用Control.BeginInvoke

        //定义回调
        private delegate void setTextValueCallBack(int value);
        //声明回调
        private setTextValueCallBack setCallBack;

        private void button1_Click(object sender, EventArgs e)
        {
            //实例化回调
            setCallBack = new setTextValueCallBack(SetValue);
            //创建一个线程去执行这个方法:创建的线程默认是前台线程
            Thread thread = new Thread(new ThreadStart(Test));
            //Start方法标记这个线程就绪了,可以随时被执行,具体什么时候执行这个线程,由CPU决定
            //将线程设置为后台线程
            thread.IsBackground = true;
            thread.Start();
        }
        private void Test()
        {
            for (int i = 0; i < 10000; i++)
            {
                //使用回调
                this.BeginInvoke(setCallBack, i);
            }
        }

        /// <summary>
        /// 定义回调使用的方法
        /// </summary>
        /// <param name="value"></param>
        private void SetValue(int value)
        {
            this.textBox1.Text = value.ToString();
        }

执行上边的代码,你会发现界面会被卡主。这里你会说,我们明明新开的线程使用的还是异步调用BeginInvoke,为什么主界面还是会卡住?

那是因为对“同步”和“异步”的理解有误造成的,那么什么是同步?什么是异步?

同步和异步是对方法执行顺序的描述。

同步:等待上一行完成计算之后,才会进入下一行。

 

例如:请同事吃饭,同事说很忙,然后就等着同事忙完,然后一起去吃饭。

 

异步:不会等待方法的完成,会直接进入下一行,是非阻塞的。

 

例如:请同事吃饭,同事说很忙,那同事先忙,自己去吃饭,同事忙完了他自己去吃饭。

这也是我看到的网上的观点,我比较认同这种观点。但不干肯定就是正确的。

新开的线程,执行到“this.BeginInvoke(setCallBack, i);”这句时,将这个消息封送到主界面线程,而且不用等待这个消息执行完就立刻返回,在很短的时间内给主线程封送了10000个消息。所以会造成主界面卡顿。而执行“this.Invoke(setCallBack, i);”这句时,将这个消息封送到主界面线程,需要等待这个消息执行完之后才能继续往下执行循环,虽然也是在很短的时间内给主线程封送了10000个消息,但还是会给主线程一点点喘息的机会,所以才不会卡段。这个也仅仅是我自己的观点,也不干保证正确。

大家可以在看一下下边代码,只比上边代码多了一句“Thread.Sleep(1);”,仅仅是让新开的线程休息0.001秒,再执行的话就不再卡顿,因为CPU的执行速度是非常快的。

 

        //定义回调
        private delegate void setTextValueCallBack(int value);
        //声明回调
        private setTextValueCallBack setCallBack;

        private void button1_Click(object sender, EventArgs e)
        {
            //实例化回调
            setCallBack = new setTextValueCallBack(SetValue);
            //创建一个线程去执行这个方法:创建的线程默认是前台线程
            Thread thread = new Thread(new ThreadStart(Test));
            //Start方法标记这个线程就绪了,可以随时被执行,具体什么时候执行这个线程,由CPU决定
            //将线程设置为后台线程
            thread.IsBackground = true;
            thread.Start();
        }
        private void Test()
        {
            for (int i = 0; i < 10000; i++)
            {
                Thread.Sleep(1);
                //使用回调
                this.BeginInvoke(setCallBack, i);
            }
        }

        /// <summary>
        /// 定义回调使用的方法
        /// </summary>
        /// <param name="value"></param>
        private void SetValue(int value)
        {
            this.textBox1.Text = value.ToString();
        }

 

像上边代码如果仅仅是给textBox框循环赋值操作的话,其实使用Application.DoEvents()就能达到不卡顿。如下代码,但一般使用Control类的Invoke 和 BeginInvoke应该不是为了这么简单的功能。

 

        private void button2_Click(object sender, EventArgs e)
        {
            button2.Enabled = false;
            for (int i = 0; i < 10000; i++)
            {
                Application.DoEvents();
                this.textBox1.Text = i.ToString();
            }
            button2.Enabled = true ;
        }

或者

        private void button2_Click(object sender, EventArgs e)
        {
            button2.Enabled = false;
            for (int i = 0; i < 10000; i++)
            {
                if (i % 100 == 0)
                {
                    Application.DoEvents();
                }  
                this.textBox1.Text = i.ToString();
            }
            button2.Enabled = true ;
        }

 

最后总结:Control类的 Invoke 或者 BeginInvoke 去调用委托的方法,两者的区别就是Invoke会导致工作线程等待,而BeginInvoke则不会等待,直接返回并继续往下执行。

 

参考网址:

https://www.cnblogs.com/worldreason/archive/2008/06/09/1216127.html

https://www.cnblogs.com/lutzmark/archive/2009/07/09/1519605.html

https://www.cnblogs.com/Rustle/articles/11301.html

https://www.cnblogs.com/w6w6/p/10648921.html

 

 

 

 

 

以上是关于Control类的Invoke 和 BeginInvoke的主要内容,如果未能解决你的问题,请参考以下文章

C#中Control的Invoke和BeginInvoke是相对于支线线程

Control.Invoke() 与 Control.BeginInvoke() [重复]

Control.Invoke() 与其委托的调用之间的延迟是多长时间?

Winform中Control的Invoke与BeginInvoke方法

浅谈Invoke 和 BegionInvoke的用法

类型安全的 Control.Invoke C#