一、背景介绍
在Windows Form中使用多线程时,除了创建控件的线程以外(就是主线程),绝对不要在任何其他线程里面调用控件的成员(只有极个别情况例外),也就是说控件属于创建它的线程,不能从其他线程里面访问。这一条适用于所有从System.Windows.Forms.Control派生的控件(因此可以说是几乎所有控件),包括Form控件本身也是。举一反三,我们很容易得出这样的结论,控件的子控件必须由创建控件的线程来创建,比如一个表单上的按钮,比如由创建表单的线程来创建,因此,一个窗口中的所有控件实际上都活在同一个线程之中。在实际编程时,大多数的软件的做法都是让同一线程负责全部的控件,这就是我们所说的UI线程(就是主线程)。
由于上述限制,我们可能会感到很不方便,的确,当我们利用一个新创建的线程来执行某些花时间的运算时,怎样知道运算进度如何并通过UI反映给用户呢?解决方法很多!比如熟悉多线程编程的用户很快会想到,我们采用一些低级的同步方法,工作者线程把状态保存到一个同步对象中,让UI线程轮询(Polling)该对象并反馈给用户就可以了。不过,这还是挺麻烦的,实际上不用这样做,Control类(及其派生类)对象有一个Invoke方法很特别,这是少数几个不受线程限制的成员之一。我们前面说到,绝对不要在任何其他线程里面调用非本线程创建的控件的成员时,也说了“只有极个别情况例外”,这个Invoke方法就是极个别情况之一----Invoke方法可以从任何线程里面调用。
二、Invoke,BeginInvoke介绍
Invoke方法的参数很简单,一个委托,一个参数表(可选),而Invoke方法的主要功能就是帮助你在UI线程(即创建控件的线程)上调用委托所指定的方法。Invoke方法首先检查发出调用的线程(即当前线程)是不是UI线程,如果是,直接执行委托指向的方法,如果不是,它将切换到UI线程,然后执行委托指向的方法。不管当前线程是不是UI线程,Invoke都阻塞直到委托指向的方法执行完毕,然后切换回发出调用的线程(如果需要的话),返回。注意,使用Invoke方法时,UI线程不能处于阻塞状态。以下MSDN里关于Invoke方法的说明:
“控件上有四种方法可以安全地从任何线程进行调用:Invoke、BeginInvoke、EndInvoke和CreateGraphics。对于所有其他方法调用,则应使用调用 (invoke) 方法之一封送对控件的线程的调用。
委托可以是 EventHandler 的实例,在此情况下,发送方参数将包含此控件,而事件参数将包含EventArgs.Empty。委托还可以是MethodInvoker的实例或采用void参数列表的其他任何委托。调用EventHandler或MethodInvoker委托比调用其他类型的委托速度更快。”
说说BeginInvoke,毫无疑问这是Invoke的异步版本(Invoke是同步完成的),BeginInvoke方法总是使用UI线程。相对Invoke而言,使用BeginInvoke稍稍麻烦一点,但还是那句话,异步比同步效果好,尽管复杂些。比如同步方法可能出现这样一种死锁情况:工作者线程通过Invoke同步调用UI线程里的方法时会阻塞,而万一UI线程正在等待工作者线程做某件事时怎么办?因此,能够使用异步方法时应尽量使用异步方法。
下面是一个具体的例子来说明Invoke和BeginInvoke的区别:
namespace ControlInvoke_BeginInvoke
{
public partial class Form1 : Form
{
private Thread myThread;
private delegate void DisplayMethod(string content);
private delegate void BeginInvokeDelegate();
string strContent = string.Empty;
public Form1()
{
InitializeComponent();
}
private void btnStart_Click(object sender, EventArgs e)
{
if (myThread != null && myThread.IsAlive)
{
this.btnStart.Text = "Stop";
myThread.Abort();
}
else
{
this.txtMain.Text = string.Empty;
string strContent = string.Empty;
this.btnStart.Text = "Start";
this.myThread = new Thread(new ThreadStart(GetContent));
this.myThread.Start();
}
}
private void GetContent()
{
this.Invoke(new DisplayMethod(Refresh_DisplayContent), "Hello World");
MessageBox.Show("BeginInvoke");
}
private void Refresh_DisplayContent(string content)
{
Thread.Sleep(5000);
lock (this.strContent)
{
this.strContent += content + "/r/n";
}
this.txtMain.Text = this.strContent;
this.txtMain.Refresh();
}
}
}
上面代码执行结果将是画面上先显示"Hello World",然后再弹出对话框。如果使用this.BeginInvoke
private void GetContent()
{
this.BeginInvoke(new DisplayMethod(Refresh_DisplayContent), "Hello World");
MessageBox.Show("BeginInvoke");
}
结果正好相反,由于BeginInvoke为异步操作,即Refresh_DisplayContent方法的执行相对于myThread线程是异步的。
三、使用场合问题
如果你的后台线程在更新一个UI控件的状态后不需要等待,而是要继续往下处理,那么你就应该使用BeginInvoke来进行异步处理。
如果你的后台线程需要操作UI控件,并且需要等到该操作执行完毕才能继续执行,那么你就应该使用Invoke。否则,在后台线程和主截面线程共享某些状态数据的情况下,如果不同步调用,而是各自继续执行的话,可能会造成执行序列上的问题,虽然不发生死锁,但是会出现不可预料的显示结果或者数据处理错误。
可以看到ISynchronizeInvoke有一个属性,InvokeRequired。这个属性就是用来在编程的时候确定,一个对象访问UI控件的时候是否需要使用Invoke或者BeginInvoke来进行封送。如果不需要那么就可以直接更新。在调用者对象和UI对象同属一个线程的时候这个属性返回false。在后面的代码分析中我们可以看到,Control类对这一属性的实现就是在判断调用者和控件是否属于同一个线程的。
四、用Reflector察看一些相关代码
1.Control.BeginInvoke and Control.Invoke
- public IAsyncResult BeginInvoke(Delegate method, params object[] args)
- {
- using (new MultithreadSafeCallScope())
- {
- return (IAsyncResult) this.FindMarshalingControl().MarshaledInvoke(this, method, args, false);
- }
- }
- public object Invoke(Delegate method, params object[] args)
- {
- using (new MultithreadSafeCallScope())
- {
- return this.FindMarshalingControl().MarshaledInvoke(this, method, args, true);
- }
- }
这里的FindMarshalingControl方法通过一个循环向上回溯,从当前控件开始回溯父控件,直到找到最顶级的父控件,用它作为封送对象。例如,我们调用窗体上一个进度条的Invoke方法封送委托,但是实际上会回溯到主窗体,通过这个控件对象来封送委托。因为主窗体是主线程消息队列相关的,发送给主窗体的消息才能发送到界面主线程消息队列。
我们可以看到Invoke和BeginInvoke方法使用了同样的实现,只是MarshaledInvoke方法的最后一个参数值不一样。
2.MarshaledInvoke
- private object MarshaledInvoke(Control caller, Delegate method, object[] args, bool synchronous)
- {
- int num;
- if (!this.IsHandleCreated)
- {
- throw new InvalidOperationException(SR.GetString("ErrorNoMarshalingThread"));
- }
- if (((ActiveXImpl) this.Properties.GetObject(PropActiveXImpl)) != null)
- {
- IntSecurity.UnmanagedCode.Demand();
- }
- bool flag = false;
- if ((SafeNativeMethods.GetWindowThreadProcessId(new HandleRef(this, this.Handle), out num) == SafeNativeMethods.GetCurrentThreadId()) && synchronous)
- {
- flag = true;
- }
- ExecutionContext executionContext = null;
- if (!flag)
- {
- executionContext = ExecutionContext.Capture();
- }
- ThreadMethodEntry entry = new ThreadMethodEntry(caller, method, args, synchronous, executionContext);
- lock (this)
- {
- if (this.threadCallbackList == null)
- {
- this.threadCallbackList = new Queue();
- }
- }
- lock (this.threadCallbackList)
- {
- if (threadCallbackMessage == 0)
- {
- threadCallbackMessage = SafeNativeMethods.RegisterWindowMessage(Application.WindowMessagesVersion + "_ThreadCallbackMessage");
- }
- this.threadCallbackList.Enqueue(entry);
- }
- if (flag)
- {
- this.InvokeMarshaledCallbacks();
- }
- else
- { //终于找到你了,PostMessage
- UnsafeNativeMethods.PostMessage(new HandleRef(this, this.Handle), threadCallbackMessage, IntPtr.Zero, IntPtr.Zero);
- }
- if (!synchronous) //如果是异步,那么马上返回吧
- {
- return entry;
- }
- if (!entry.IsCompleted) //同步调用没结束,阻塞起来等待吧
- {
- this.WaitForWaitHandle(entry.AsyncWaitHandle);
- }
- if (entry.exception != null)
- {
- throw entry.exception;
- }
- return entry.retVal;
- }
怎么样,我们终于看到PostMessage了吧?通过windows消息机制实现了封送。而需要封送的委托方法作为消息的参数进行了传递。关于其它的代码这里不作进一步解释。
3.InvokeRequired
- public bool InvokeRequired
- {
- get
- {
- using (new MultithreadSafeCallScope())
- {
- HandleRef ref2;
- int num;
- if (this.IsHandleCreated)
- {
- ref2 = new HandleRef(this, this.Handle);
- }
- else
- {
- Control wrapper = this.FindMarshalingControl();
- if (!wrapper.IsHandleCreated)
- {
- return false;
- }
- ref2 = new HandleRef(wrapper, wrapper.Handle);
- }
- int windowThreadProcessId = SafeNativeMethods.GetWindowThreadProcessId(ref2, out num);
- int currentThreadId = SafeNativeMethods.GetCurrentThreadId();
- return (windowThreadProcessId != currentThreadId);
- }
- }
- }
终于看到了,这是在判断windows窗体线程和当前的调用者线程是否是同一个,如果是同一个就没有必要封送了,直接访问这个GUI控件吧。否则,就不要那么直接表白了,就需要Invoke或者BeginInvoke做媒了。