讨论环境:C# 、.net、VS2005
.net默认所有的可视窗体在主线程内维护,如果某工作线程(主线程之外)想实现对窗体控件的操作,缺省情况下是不允许直接操作的,而要通过 Invoke 方法将其封送到主线程去完成。在Control 类内提供了 Invoke 和 BeginInvoke 两个方法实现该功能,MSDN 帮助中提到,它们的唯一区别是 BeginInvoke 多了“异步执行”四个字。(两方法的具体帮助请自行查看MSDN,这里不多罗嗦了)。
“异步执行”怎么理解,查了网上的一些解答,通过Reflector查看了两方法的背后源码后,得出如下结论:
Invoke 引起工作线程的阻塞,BeginInvoke 不引起工作线程的阻塞。
具体解释一下:我们先假设称主线程(即窗体控件的拥有者)为A线程,工作线程为B线程,如果B线程需要操作窗体控件,那么就要使用 Invoke(或BeginInvoke),将相应的操作通过代理,封送到主线程A(具体的代码实现,不多罗嗦,假设读者已知)。那么.net背后是怎么实现线程间“任务挪移”这一步操作的呢?通过Reflector查看源码后发现,原来 Invoke 将你交给它的委托封装成了一个标准的Windows消息,加进了主线程的消息队列内。回到 Invoke 和 BeginInvoke 的区别上来,如果使用 Invoke,那么 B 线程必须等到A线程响应了传送的消息后 才能得到返回值,而如果使用BeginInvoke,则B线程将消息送到A线程后,马上返回,并不一定等待该消息被A线程响应完成。所以如果A线程处在繁忙状态或休眠状态,使用 Invoke 封送消息就会使得B线程被堵塞,而是用 BeginInvoke 则不然,这就是所谓的“异步执行”了。
空口无凭,让我们来看看Invoke和 BeginInvoke 背后的代码:
public object Invoke(Delegate method, params object[] args)
{
using (MultithreadSafeCallScope scope = new MultithreadSafeCallScope())
{
return this.FindMarshalingControl().MarshaledInvoke(this, method, args, true);
}
}
public IAsyncResult BeginInvoke(Delegate method, params object[] args)
{
using (MultithreadSafeCallScope scope = new MultithreadSafeCallScope())
{
return (IAsyncResult) this.FindMarshalingControl().MarshaledInvoke(this, method, args, false);
}
}
两方法对应的代码基本一样,除了返回类型外,还有一处细微的差别 MarshaledInvoke 方法的第三个参数:Invoke 为 true,BeginInvoke 为 false。这个参数表示什么意思呢?把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
{
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;
}
好长的一段代码,看着都眼晕。看看刚才提到的第三个参数的名字 synchronous ,从字面意思看‘是否同步’,OK,再看看最后被加粗的几行代码(还是根据字面意思猜):如果不同步,立刻返回;如果同步,而且没完事,就等一等,最后给出返回值。如果我们的假设成立,那么 Invoke 方法的第三个参数为True,就是要同步;BeginInvoke 方法的第三个参数为 False,就是不同步,即异步。Right,到这里就和 MSDN 帮助上介绍 BeginInvoke “异步执行”就对上了。
如果按照数学分析归纳法,到此应该说:由以上分析,得证:
Invoke 引起工作线程的阻塞,BeginInvoke 不引起工作线程的阻塞。
最后,要补充两点:第一,如果主线程以外的工作线程要操作窗体控件,并非一定要使用Invoke方法,更改下边这个属性也是 OK 的,只是不推荐使用。
System.Windows.Forms.Control.CheckForIllegalCrossThreadCalls = false;
第二,既然 BeginInvoke 不引起阻塞,那么是否就说明它比 Invoke 好呢。我在自己的程序里,把所有的 Invoke 均变成 BeginInvoke ,运行一段时间后,结果提示 StackOverFlowException 错误,帮助对这个异常的说明是“挂起的方法调用过多而导致执行堆栈溢出时引发的异常”。看来还是要慎用。
生活TMD需要激情,做事需冷静,说话需冷静!
遇事记着:办法总比困难多,困难和问题说不定就是机遇和转折!
历史证明:哪个环节没照顾到,哪个环节就会出问题!能自己来,就不要让别人来。