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() 与其委托的调用之间的延迟是多长时间?