问题的引入
下面有个简单的demo,大家一看代码就知道效果如何示例。我新建一个winform的程序,然后写入了如下代码:
using System;
using System.Windows.Forms; namespace MyExampleList
{
public partial class ControlInvokeExample : Form
{
public ControlInvokeExample()
{
InitializeComponent();
} private void btnSayHello_Click(object sender, EventArgs e)
{
txtHello.Text = "祝你学习C#愉快";
}
}
}
运行效果如下:
[caption id="attachment_1150" align="alignnone" width="364"] 显示文本[/caption]
下面我来改造一下这个简单的demo。我们在编程的时候使用多线程是非常的常见,我们在点击按钮的时候启用另一个线程,然后来操作textbox的值,让它进行变化,代码如下:
using System;
using System.Threading;
using System.Windows.Forms; namespace MyExampleList
{
public partial class ControlInvokeExample : Form
{
public ControlInvokeExample()
{
InitializeComponent();
} private void btnSayHello_Click(object sender, EventArgs e)
{
Thread th=new Thread(new ThreadStart(StartMethord));
th.Start(); } private void StartMethord()
{
txtHello.Text = "祝你学习C#愉快";
}
}
}
理论上就应该是上面的代码就可以执行的,但是实际上会报错,具体错误如下图所示:
[caption id="attachment_1152" align="alignnone" width="860"] 线程交互错误[/caption]
错误解释:上面的错误意思就是txtHello这个控件不是th创建的所以th线程不能直接访问它,因此报错了。
那我们如何让th线程也能改变UI中的txtHello这个textbox的值呢?
我们通过Control.Invoke或者Control.BeginInvoke来实现。
Control.Invoke和Control.BeginInvoke
Control.Invoke:在拥有此控件的基础窗口句柄的线程上执行指定的委托。
Control.BeginInvoke:在创建控件的基础句柄所在线程上异步执行指定委托。
根据上面的解释我们来修改一下上面出错的代码,用Invoke的方式来实现th线程更改txtHello的内容,具体代码如下(我称下面的这种实现方式为:A):
using System;
using System.Threading;
using System.Windows.Forms; namespace MyExampleList
{
public partial class ControlInvokeExample : Form
{
public ControlInvokeExample()
{
InitializeComponent();
}
delegate void InvokeDelegate();
private void btnSayHello_Click(object sender, EventArgs e)
{
Thread th=new Thread(new ThreadStart(StartMethord));
th.Start();
}
private void StartMethord()
{
txtHello.Invoke(new InvokeDelegate(ChangeTextBox));
}
private void ChangeTextBox()
{
txtHello.Text = "祝你学习C#愉快";
}
}
}
用BeginInvoke的方式来实现th线程更改txtHello的内容,具体代码如下(我称下面的这种实现方式为:B):
using System;
using System.Threading;
using System.Windows.Forms; namespace MyExampleList
{
public partial class ControlInvokeExample : Form
{
public ControlInvokeExample()
{
InitializeComponent();
}
delegate void InvokeDelegate();
private void btnSayHello_Click(object sender, EventArgs e)
{
Thread th=new Thread(new ThreadStart(StartMethord));
th.Start();
}
private void StartMethord()
{
txtHello.BeginInvoke(new InvokeDelegate(ChangeTextBox));
}
private void ChangeTextBox()
{
txtHello.Text = "祝你学习C#愉快";
}
}
}
我们对比一下A和B两种实现方式,在代码上的区别,唯一的区别就是A使用了txtHello.Invoke和B方式用了txtHello.BeginInvoke,也就是他们的区别又归到了Invoke和BeginInvoke上,一个是同步,一个是异步,也就是A方式的时候是等待Invoke的执行结果,而B方式的时候是直接执行下去不等待BeginInvoke的执行结果。在本例的demo中方式A和B实现的效果相同,但是原理不同,仅此而已。下面我来重新写一个新的demo,让大家见识一下这两个方法的区别。
修改后的实例代码如下:
using System;
using System.Threading;
using System.Windows.Forms; namespace MyExampleList
{
public partial class ControlInvokeExample : Form
{
public ControlInvokeExample()
{
InitializeComponent();
}
delegate void InvokeDelegate();
private void btnSayHello_Click(object sender, EventArgs e)
{
Thread th=new Thread(new ThreadStart(StartMethord));
th.Start(); }
private void StartMethord()
{
DateTime dt1 = DateTime.Now;
txtHello.Invoke(new InvokeDelegate(ChangeTextBox));//等待ChangeTextBox方法执行完毕,所以str的值比较大
// txtHello.BeginInvoke(new InvokeDelegate(ChangeTextBox));//不等待ChangeTextBox方法执行完毕,所以str的值会很小
string str= (DateTime.Now - dt1).TotalMilliseconds.ToString(); }
private void ChangeTextBox()
{
Thread.Sleep(3000);
txtHello.Text = "祝你学习C#愉快"+DateTime.Now.ToString();
}
}
}
运行后的结果如下两图:
[caption id="attachment_1154" align="alignnone" width="630"] invoke结果[/caption]
[caption id="attachment_1155" align="alignnone" width="584"] BeginInvoke结果[/caption]
差距很明显了吧。
那么什么时候我们必须使用Invoke和BeginInvoke呢?
InvokeRequired:获取一个值,该值指示调用方在对控件进行方法调用时是否必须调用 Invoke 方法,因为调用方位于创建控件所在的线程(本例中的UI线程)以外的线程(自定义线程th)中。
也就是当InvokeRequired为true时我们必须用Invoke或者BeginInvoke方法来更新界面。
特别注意
Control的Invoke和BeginInvoke的委托方法是在主线程,即UI线程上执行的。也就是说如果你的委托方法用来取花费时间长的数据,然后更新界面什么的,千万别在UI线程上调用Control.Invoke和Control.BeginInvoke,因为这些是依然阻塞UI线程的,造成界面的假死。
那么,这个异步到底是什么意思呢?(具体的解释可以看上面的A和B方式,指的是th线程的异步)
异步是指相对于调用BeginInvoke的线程异步,而不是相对于UI线程异步,你在UI线程上调用BeginInvoke ,当然不行了。----摘自"Invoke和BeginInvoke的真正涵义"一文中的评论。
BeginInvoke的原理是将调用的方法Marshal成消息,然后调用Win32 API中的RegisterWindowMessage()向UI窗口发送消息。----摘自"Invoke和BeginInvoke的真正涵义"一文中的评论。