原文
标题:Asynchronous Method Invocation
作者:mikeperetz
链接:http://www.codeproject.com/csharp/AsyncMethodInvocation.asp
翻译开始时间:2007-11-27
简介
在此篇文章,我将要解释异步方法的调用以及如何使用它们。在玩过委托、线程和异步调用一段时间后,如果我不分享一下我在这些方面获得的一些知识的话,我会有罪恶感地;很幸运地,你无需在半夜1点时还看着MSDN文章,在想为什么你还在电脑前。我会尝试用很多的示例,从最基础的讲起……整体上,我会覆盖以下知识点:如何异步调用方法、如何向这些方法传入参数,以及让你能了解方法是如何完成执行的。最后,我会用使用一些简单的代码实现命令模式(Command Pattern)。在.NET中使用异步调用方法的最大好处在于你能执行在你的项目里的任何一个方法,而且你能对它实行异步调用而无需改动原来的代码。虽然,绝大部分的原理都封装在.NET框架内,了解背后的如何执行的机制非常重要。就这样,我们学习先从这里开始。
同步 vs 异步
让我尝试用实例解释同步和异步调用方法,因为我知道你们上Code Project都喜欢直接看代码,而不是读《War and Peace》(我没有任何针对此书的意思)。
同步调用
假想我们现在有一个方法Foo,执行需要花10秒钟。
private void Foo()
{
// sleep for 10 seconds.
Thread.Sleep(10000);
}
正常情况下,当程式调用Foo方法时,需要等待10秒钟直到Foo结束,控制权才会返回到正在请求的线程中。现在,假设你要调用Foo方法100遍,那么我们知道,这需要花1000秒,控制权才会返回到正在请求的线程。这种调用方法的类型就是“同步”。
1、呼叫 Foo()
2、Foo() 被执行
3、控件权回到请求线程
我们在这里大部分工作都基于委托的,所以我们使用委托调用Foo方法。幸运的是,在.NET的框架内,已经有一个允许我们在无参数和无返回值的条件下调用方法。这个委托名为MethodeInvoker(在System.Windows.Forms命名空间下)。我们用它玩一下。
// create a delegate of MethodInvoker poiting
// to our Foo() function.
MethodInvoker simpleDelegate = new MethodInvoker(Foo);
// Calling Foo
simpleDelegate.Invoke();
即使是以上的示例代码,我们还是同步地调用了Foo。正在请求的线程还是需要等待Invoke()方法完全执行完毕才能获取控制权。
异步调用
然而,如果我想在调用Foo()后,无需等待它的执行完毕呢?其实,让事情更有趣的是,如果我并不关心它是否已经完成执行了?那就是说,我刚才想调用Foo 100遍,而无需等待任何方法的完成。基本上,这种处理事情的方法称之为“发后不理”(Fire and Forget)(Wiki解释:泛指武器在發射之後,就不再接受任何外界指揮、管制或者是射控系統的資料,更新自己的座標或者是目標的訊息。發射武器的載具能夠進行其他的作業,包括搜索標定下一個目標,或者是離開發射地點。 程式上就是,触发方法后,无需再管它任何运行状态。)你调用方法,不需要等待它们,并且你可以直接忽略它。但,我们不要忘记!我们并不想更改我那超级复杂梦幻的Foo方法的一行代码。
// create a delegate of MethodInvoker poiting to
// our Foo function.
MethodInvoker simpleDelegate = new MethodInvoker(Foo);
// Calling Foo Async
for(int i=0; i<100; i++)
simpleDelegate.BeginInvoke(null, null);
我来为上面的代码做一点解释:
1、注意 BeginInvoke() 的那行代码,它在执行Foo方法。然而,在没有等Foo()执行完毕,立即返回控制到调用处。
2、以上代码并不知道某一个Foo()方法何时执行完毕,我会在后文阐述。
3、用 BeginInvoke() 替换 Invoke()。暂时,无需担心方法所需要的参数如何传入;我会在后文更详细地讨论。
.NET在后台施了什么魔法?
一旦你要求框架异步调用某方法,这就需要线程完成这工作。这不能是当前的线程,因为那是同步调用(会被阻塞)。取而代之,程序运行时会对需要执行的方法进行列队,向.NET的线程池请求获取线程执行这些方法。你不需要为了这些写任何代码,所有的这些都会发生在后台。然而,只是因为,它是全透明操作就不意味着你需要关注它,这里有几点需要记住:
1、Foo()方法执行在一个分离的线程上,这条线成是属于。NET的线程池的。
2、.NET线程池默认拥有25条线程(你可以修改这个限制),每次Foo()执行的时候,就会使用线程池的其中一条线程。但你不能选择使用哪条线程。
3、线程池是有限制的!一旦所有的线程都被占用的时候,下一个异步方法调用只能等待线程池的其中一条线程转为空置状态才能使用。这种状态称为“线程池资源缺乏”(Thread Pool Starvation),如果出现了这种状况,就会影响性能。
不要潜入线程池太深,你会喘不过气的!
让我们看个实例,引发线程池资源匮乏。修改Foo方法,执行时间为30秒,同时让他输出一下信息:
1、线程池中剩下可用线程数量
2、线程是否在线程池中
3、线程的ID
首先,我们知道线程池拥有25条线程,因此,我将异步调用30次Foo方法(观察第25次调用之后发生了什么)。
private void CallFoo30AsyncTimes()
{
// create a delegate of MethodInvoker
// poiting to our Foo function.
MethodInvoker simpleDelegate =
new MethodInvoker(Foo);
// Calling Foo Async 30 times.
for (int i = 0; i < 30; i++)
{
// call Foo()
simpleDelegate.BeginInvoke(null, null);
}
}
private void Foo()
{
int intAvailableThreads, intAvailableIoAsynThreds;
// ask the number of avaialbe threads on the pool,
//we really only care about the first parameter.
ThreadPool.GetAvailableThreads(out intAvailableThreads,
out intAvailableIoAsynThreds);
// build a message to log
string strMessage =
String.Format(@"Is Thread Pool: {1},
Thread Id: {2} Free Threads {3}",
Thread.CurrentThread.IsThreadPoolThread.ToString(),
Thread.CurrentThread.GetHashCode(),
intAvailableThreads);
// check if the thread is on the thread pool.
Trace.WriteLine(strMessage);
// create a delay
Thread.Sleep(30000);
return;
}
输出窗口:
Is Thread Pool: True, Thread Id: 7 Free Threads 24
Is Thread Pool: True, Thread Id: 12 Free Threads 23
Is Thread Pool: True, Thread Id: 13 Free Threads 22
Is Thread Pool: True, Thread Id: 14 Free Threads 21
Is Thread Pool: True, Thread Id: 15 Free Threads 20
Is Thread Pool: True, Thread Id: 16 Free Threads 19
Is Thread Pool: True, Thread Id: 17 Free Threads 18
Is Thread Pool: True, Thread Id: 18 Free Threads 17
Is Thread Pool: True, Thread Id: 19 Free Threads 16
Is Thread Pool: True, Thread Id: 20 Free Threads 15
Is Thread Pool: True, Thread Id: 21 Free Threads 14
Is Thread Pool: True, Thread Id: 22 Free Threads 13
Is Thread Pool: True, Thread Id: 23 Free Threads 12
Is Thread Pool: True, Thread Id: 24 Free Threads 11
Is Thread Pool: True, Thread Id: 25 Free Threads 10
Is Thread Pool: True, Thread Id: 26 Free Threads 9
Is Thread Pool: True, Thread Id: 27 Free Threads 8
Is Thread Pool: True, Thread Id: 28 Free Threads 7
Is Thread Pool: True, Thread Id: 29 Free Threads 6
Is Thread Pool: True, Thread Id: 30 Free Threads 5
Is Thread Pool: True, Thread Id: 31 Free Threads 4
Is Thread Pool: True, Thread Id: 32 Free Threads 3
Is Thread Pool: True, Thread Id: 33 Free Threads 2
Is Thread Pool: True, Thread Id: 34 Free Threads 1
Is Thread Pool: True, Thread Id: 35 Free Threads 0
Is Thread Pool: True, Thread Id: 7 Free Threads 0
Is Thread Pool: True, Thread Id: 12 Free Threads 0
Is Thread Pool: True, Thread Id: 13 Free Threads 0
Is Thread Pool: True, Thread Id: 14 Free Threads 0
Is Thread Pool: True, Thread Id: 15 Free Threads 0
对于输出结果我们要注意几点:
1、注意,首先,全部的线程都来自线程池。
2、注意每次Foo被调用时,另一个线程的的ID被分配。然而,你能看到某些线程在循环使用。
3、呼叫Foo()25次以后,你可以看到池内已没有了空闲的线程。对于这点,程式只能等待有空闲的线程为止。
4、一旦有线程空置,程式立刻抓取这条线成,继续呼叫Foo()方法,并且在池内持续保持0个空闲线程。这状况持续发生直到Foo被呼叫30次。
立刻回过头来,我们不做太怪异的东西,我们记下几条关于异步调用方法的意见:
1、知道了你的代码将会运行在一条分离的线程中,因此一些关于线程安全的问题会被提出。这是属于另一个领域的问题,我不会在这里讨论。
2、记住,线程池是有限制的。如果你计划异步调用很多方法,并且假设他们都需要很长的执行时间,Thread Pool Starvation 可能会发生。
BeginInvoke() 和 EndInvoke()
目前为止,我们看到一个方法在无需知道它何时结束的情况下,调用它。如果再加上EndInvoke()的话,那就有可能做多点东西。首先,EndInvoke将会阻塞直到你的方法执行完毕;因此,调用BeginInvoke后紧接着调用EndInvoke,就非常像在一个堵塞模式内调用方法(因为EndInvoke将会等待直到你的方法执行完毕)。然而,.NET的运行时怎么知道如何绑定BeginInvoke和EndInvoke呢?IAsyncResult现在就在此出现了。当调用BegineInvoke的时候,返回值的类型为IAsyncResult;这是一个允许框架跟踪你方法执行的“粘合剂”。可以把它想象为一个小标签,可以让你知道你方法的执行状况。使用这个小小的超级强大标签,你可以知道你的方法何时执行完毕;以及,你也能使用这个标签,附加任何你想传入正在执行方法的对象。好!我们看看一些示例你就不会太迷惑了。我们先创建一个新的Foo方法。
private void FooOneSecond()
{
// sleep for one second!
Thread.Sleep(1000);
}
private void UsingEndInvoke()
{
// create a delegate of MethodInvoker poiting to our Foo function.
MethodInvoker simpleDelegate = new MethodInvoker(FooOneSecond);
// start FooOneSecond, but pass it some data this time!
// look at the second parameter
IAsyncResult tag =
simpleDelegate.BeginInvoke(null, "passing some state");
// program will block until FooOneSecond is complete!
simpleDelegate.EndInvoke(tag);
// once EndInvoke is complete, get the state object
string strState = (string)tag.AsyncState;
// write the state object
Trace.WriteLine("State When Calling EndInvoke: "
+ tag.AsyncState.ToString());
}
如果发生异常,我要怎么捕获?
现在,我们来点复杂一点点的。我们修改FooOneSecond方法,让它抛出异常。你计划要捕获这个异常。在BeginInvoke内,还是在EndInvoke?或者根本没有可能捕获这个异常呢?首先,不会在BeginInvoke内。BeginInvoke的任务就是在线程池内简单地开始执行方法。EndInvoke的真正工作是报告关于方法的完成的全部信息,包括异常。注意接下来的代码片断:
private void FooOneSecond()
{
// sleep for one second!
Thread.Sleep(1000);
// throw an exception
throw new Exception("Exception from FooOneSecond");
}
现在,我们呼叫FooOneSecond,看看我们是否能捕捉这个异常。
private void UsingEndInvoke()
{
// create a delegate of MethodInvoker poiting
// to our Foo function.
MethodInvoker simpleDelegate =
new MethodInvoker(FooOneSecond);
// start FooOneSecond, but pass it some data this time!
// look at the second parameter
IAsyncResult tag = simpleDelegate.BeginInvoke(null, "passing some state");
try
{
// program will block until FooOneSecond is complete!
simpleDelegate.EndInvoke(tag);
}
catch (Exception e)
{
// it is here we can catch the exception
Trace.WriteLine(e.Message);
}
// once EndInvoke is complete, get the state object
string strState = (string)tag.AsyncState;
// write the state object
Trace.WriteLine("State When Calling EndInvoke: "
+ tag.AsyncState.ToString());
}
运行代码后,你会看到,异常捕捉只在呼叫EndInvoke的时候。如果你决定从不呼叫EndInvoke,那你将得不到异常。然而,当代码在Debugger状态下运行时,就看你对异常的设定,异常抛出时有可能会使调试中断。但那是调试器。使用发布版本的时候,如果你不调用EndInvoke,那你将永远得不到异常。
向你的方法传入参数
好,像这样调用无参数的方法不会让我们走得太远,因此,我要为那超级怪异的Foo方法添加一些参数。
private string FooWithParameters(string param1,
int param2, ArrayList list)
{
// lets modify the data!
param1 = "Modify Value for param1";
param2 = 200;
list = new ArrayList();
return "Thank you for reading this article";
}
我们使用BeginInvoke和EndInvoke调用FooWithParameters。首先,我们先定义一个与方法对应签名的委托。
public delegate string DelegateWithParameters(string param1,
int param2, ArrayList list);
想象BeginInvoke和EndInvoke把我们的方法切分为两个方法。BeginInvoke负责接收每个BeginInvoke都拥有的2个额外输入参数(使用委托和状态对象)。EndInvoke负责输出参数(标记为ref或out的参数)和返回值。如果它有一个的话。我们回头看看我们的示例,看看什么是传入参数和什么是输出参数。param1, param2, 和 list 都为传入参数,因此,他们将被BeginInvoke方法作为参数接纳。输出值为字符串,可以认为是输出参数,因此,这将会为EndInvoke返回值的类型。最酷的事情就是,编译器会根据你生命的委托,为BeginInvoke和EndInvoke生成正确的方法签名。注意,我现在要修改传入参数的值,只是要验证一下行为是否在不调用BeginInvoke和EndInvoke的状况下正常执行。我也会重新分配ArrayList传入。这样,猜猜我们的输出是什么?
private void CallFooWithParameters()
{
// create the paramets to pass to the function
string strParam1 = "Param1";
int intValue = 100;
ArrayList list = new ArrayList();
list.Add("Item1");
// create the delegate
DelegateWithParameters delFoo =
new DelegateWithParameters(FooWithParameters);
// call the BeginInvoke function!
IAsyncResult tag =
delFoo.BeginInvoke(strParam1, intValue, list, null, null);
// normally control is returned right away,
// so you can do other work here
// calling end invoke to get the return value
string strResult = delFoo.EndInvoke(tag);
// write down the parameters:
Trace.WriteLine("param1: " + strParam1);
Trace.WriteLine("param2: " + intValue);
Trace.WriteLine("ArrayList count: " + list.Count);
}
方便你不用再回滚你的鼠标,我们在这里再看看FooWithParameters。
private string FooWithParameters(string param1,
int param2, ArrayList list)
{
// lets modify the data!
param1 = "Modify Value for param1";
param2 = 200;
list = new ArrayList();
return "Thank you for reading this article";
}
在呼叫EndInvoke的之后,输出窗口的3行结果为:
param1: Param1
param2: 100
ArrayList count: 1
我们分析一下。即使当我们修改了输入参数的值,我们在EndInvoke之后也看不到全部的变更。字符串是可变类型,因此,一份字符串的拷贝产生了,方法所作的更改传递不到EndInvoke之后。整数为之类型,当他传值的时候也会产生复本。最后,重新创建的ArrayList也没有返回到EndInvoke,因为传递ArrayList传递它可的引用,并且事实上,重新创建一个ArrayList就等效于为ArrayList重新分配一个新的引用以供传递。事实上,引用丢失了,也可以认为是内存泄漏;但幸运的是,.NET的垃圾回收将会最终收集它。因此,如果我们想获取那个新的ArrayList和其余我们所做的对参数的修改,我们需要怎么做呢?简单,我们在ArrayList加上ref即可。我们也加上output参数看看EndInvoke如何被更改。
private string FooWithOutAndRefParameters(string param1,
out int param2, ref ArrayList list)
{
// lets modify the data!
param1 = "Modify Value for param1";
param2 = 200;
list = new ArrayList();
return "Thank you for reading this article";
}
我们看看,哪个是输出参数,哪个是输入参数……
1、Param1 为输入参数,它只在BeginInvoke接受。
2、Param2 既是输入参数也是输出参数;因此它可以传递于BeginInvoke和EndInvoke(EndInvoke会给我们更新后的值)。
3、list 引用地址传递,因此它可以传递于BeginInvoke和EndInvoke。
我们的委托现在为这样:
public delegate string DelegateWithOutAndRefParameters(string param1,
out int param2, ref ArrayList list);
最后,我们调用FooWithOutAndRefParameters
private void CallFooWithOutAndRefParameters()
{
// create the paramets to pass to the function
string strParam1 = "Param1";
int intValue = 100;
ArrayList list = new ArrayList();
list.Add("Item1");
// create the delegate
DelegateWithOutAndRefParameters delFoo =
new DelegateWithOutAndRefParameters(FooWithOutAndRefParameters);
// call the beginInvoke function!
IAsyncResult tag =
delFoo.BeginInvoke(strParam1,
out intValue,
ref list,
null, null);
// normally control is returned right away,
// so you can do other work here
// calling end invoke notice that intValue and list are passed
// as arguments because they might be updated within the function.
string strResult =
delFoo.EndInvoke(out intValue, ref list, tag);
// write down the parameters:
Trace.WriteLine("param1: " + strParam1);
Trace.WriteLine("param2: " + intValue);
Trace.WriteLine("ArrayList count: " + list.Count);
Trace.WriteLine("return value: " + strResult);
}
输出:
param1: Param1
param2: 200
ArrayList count: 0
return value: Thank you for reading this article
注意param1并没有改变。param2以输出参数传出现在更新为200。ArrayList也被重新配置了,我们看到它指向一个新的0个参数集合引用(原来的引用地址丢失了)。我希望现在你能明白参数在BeginInvoke和EndInvoke是如何传递的。让我们移到另一个角度,看看如何在非阻塞方法完成的时候得到通知。
IAsyncResult隐藏什么你想知道的东西?
你可能想知道EndInvoke怎样提供给我们输出参数,以及获取更新的ref参数。或者,EndInvoke是怎么抛出方法抛出的异常的。举例,我们用BegineInvoke调用Foo,然后Foo执行完毕,现在,我们一般会执行EndInvoke,但我们决定在Foo执行了20分钟后完成时调用EndInvoke会怎么样呢?EndInvoke仍旧会提供给你输出和ref的参数,仍旧会抛出异常(如果有异常的话)。那这些信息存在哪里呢?EndInvoke怎么能够在方法运行长时间后获取这些全部的信息?好……关键在于IAsyncResult!关于这个对象,我决定探讨多一点;如我猜测,这就是能保存所有关于你方法调用的所有信息。注意EndInvoke接受一个参数,类型为IAsyncResult。这个对象包含的信息如下所示:
1、方法执行完毕了吗?
2、委托引用被BeginInvoke使用
3、所有的输出参数和他们的值
4、所有的ref参数和他们更新后的值
5、返回值
6、异常
7、更多……
IAsyncResult看起来好像没什么信息,因为它只是一个只有几个属性的接口,但实际,他的类型为System.Runtime.Remoting.Messaging.AsyncResult。
如果我们再挖掘深一点,我们会发现AsyncResult包括一个类型为System.Runtime.Remoting.Messaging.ReturnMessage名叫_replyMsg的对象,你梦寐以求的东西在这里找到了!
我们能清楚地看到我们返回值、输出参数和ref参数。甚至有个exception属性存储异常。注意我扩展的,在调试窗口看到OutArgs,值200和新分配的ArrayList的引用。你也看到一个ReturnValue的属性,字符串为“Thank you for reading this article”。如果有异常,EndInvoke会为我们抛出。我想,这些已经足够证明全部关于方法调用的信息都存储在你从BeginInvoke获取的IAsyncResult对象内,它就像一把钥匙开启你的数据。如果我们失去这个对象,我们将不会得到输出参数,ref参数和返回值;也不可能捕捉到任何异常。这是关键!你失去了他,这些信息就会永远迷失在.NET的运行时里了……好啦,这个就暂时告一段落。我想我已经达到我的目的了。
使用回调委托,Hollywood风格“不要找我,我会通知你”到这里,你应该知道怎么传递参数,怎么传递状态,以及明白方法执行在线程池中某条线程的原理。有一件事情我并没有提起的就是,当方法运行完成时获取通知的方法。毕竟,在一个封闭模块中等待方法完成这样的做法是不完美的。当方法完成的时候,为了获通知,你必须使用一个在BeginInvoke的回调委托。好,例子来啦!看看以下两个方法:
private void CallFooWithOutAndRefParametersWithCallback()
{
// create the paramets to pass to the function
string strParam1 = "Param1";
int intValue = 100;
ArrayList list = new ArrayList();
list.Add("Item1");
// create the delegate
DelegateWithOutAndRefParameters delFoo =
new DelegateWithOutAndRefParameters(FooWithOutAndRefParameters);
delFoo.BeginInvoke(strParam1,
out intValue,
ref list,
new AsyncCallback(CallBack), // callback delegate!
null);
}
private void CallBack(IAsyncResult ar)
{
// define the output parameter
int intOutputValue;
ArrayList list = null;
// first case IAsyncResult to an AsyncResult object, so we can get the
// delegate that was used to call the function.
AsyncResult result = (AsyncResult)ar;
// grab the delegate
DelegateWithOutAndRefParameters del =
(DelegateWithOutAndRefParameters) result.AsyncDelegate;
// now that we have the delegate,
// we must call EndInvoke on it, so we can get all
// the information about our method call.
string strReturnValue = del.EndInvoke(out intOutputValue,
ref list, ar);
}
在这里,你看到当我们呼叫BeginInvoke的时候,传递了一个委托方法CallBack。.NET在方法FooWithOutAndRefParameters完成的时候会通知我们。在之前,我们都知道,如果我们要获取我们的输出参数,必须要调用EndInvoke。我们为了调用EndInvoke,需要在委托上做一些技巧。
AsyncResult result = (AsyncResult)ar;
// grab the delegate
DelegateWithOutAndRefParameters del =
(DelegateWithOutAndRefParameters) result.AsyncDelegate;
等等!回调函数执行在哪一条线程上?毕竟,.NET使用你的委托执行了回调,归根到底,还是.NET在调用你的委托。你有权利和义务知道你的代码在哪条线程上运行。为了弄清楚这一切,我再一次修改Foo方法,让他显示相关的线程信息和延时4秒。
private string FooWithOutAndRefParameters(string param1,
out int param2, ref ArrayList list)
{
// log thread information
Trace.WriteLine("In FooWithOutAndRefParameters: Thread Pool? "
+ Thread.CurrentThread.IsThreadPoolThread.ToString() +
" Thread Id: " + Thread.CurrentThread.GetHashCode());
// wait for 4 seconds as if this functions takes a while to run.
Thread.Sleep(4000);
// lets modify the data!
param1 = "Modify Value for param1";
param2 = 200;
list = new ArrayList();
return "Thank you for reading this article";
}
我也会在callback方法添加线程信息:
private void CallBack(IAsyncResult ar)
{
// which thread are we on?
Trace.WriteLine("In Callback: Thread Pool? "
+ Thread.CurrentThread.IsThreadPoolThread.ToString() +
" Thread Id: " + Thread.CurrentThread.GetHashCode());
// define the output parameter
int intOutputValue;
ArrayList list = null;
// first case IAsyncResult to an AsyncResult object,
// so we can get the delegate that was used to call the function.
AsyncResult result = (AsyncResult)ar;
// grab the delegate
DelegateWithOutAndRefParameters del =
(DelegateWithOutAndRefParameters) result.AsyncDelegate;
// now that we have the delegate, we must call EndInvoke on it, so we
// can get all the information about our method call.
string strReturnValue = del.EndInvoke(out intOutputValue, ref list, ar);
}
我在Form中的Button,触发执行FooWithOutAndRefParameters多次。
private void button4_Click(object sender, EventArgs e)
{
CallFooWithOutAndRefParametersWithCallback();
}
让我们看看点击按钮3此后的输出(即3次执行方法):
In FooWithOutAndRefParameters: Thread Pool? True Thread Id: 7
In FooWithOutAndRefParameters: Thread Pool? True Thread Id: 12
In FooWithOutAndRefParameters: Thread Pool? True Thread Id: 13
In Callback: Thread Pool? True Thread Id: 7
In Callback: Thread Pool? True Thread Id: 12
In Callback: Thread Pool? True Thread Id: 13
注意我的Foo方法执行了3次,在3条线程上交替执行。全部的线程都属于线程池内。Callback方法也分别执行了3次,他们也全部使用线程池内的线程。有趣的是,Callback执行的线程ID看起来和Foo的一样。Foo执行“线程7”,4秒后,Callback也同样执行在“线程7”。一样的情况下出现在“线程12”和“线程13”。Callback方法就像Foo方法的继续。我多次点击按钮,尝试看看Callback方法是否会调用与Foo调用的不同,结果并没有发现。如果你考虑一下,其实一切都合乎情理。想象,.NET会抓著一条线程,执行Foo后再抓另外一条线程执行Callback,这样太浪费了!线程池资源匮乏的情况,你还要等待一条新线程只为了调用Callback。这将会是一场灾难。
使用“命令模式”整理一下知识
看看前面的,是不是有点混乱呢。我们提到了 BeginInvoke, EndInvoke,callback,他们都混在一起了。让我们尝试使用命令模式整理方法的调用。使用命令模式非常简单。基本上,你创建一个命令类需要实现一个很简单的接口,类似:
public interface ICommand
{
void Execute();
}
是时间停止到处使用我们那无意义的Foo方法了,我们做一些更真实的东西。现在,我们创建一个更真实的场景。我们拥有:
1、我们有一个用户窗体包含了显示客户行数的表格。
2、表格会根据搜索客户的关键ID更新显示的行数。然而,数据库在很远的地方,要花5秒的时间才能得到客户的DataSet;我们不希望在等待的时候UI会停顿。
3、我们有一个很好的业务对象,其职责为根据客户ID获取我们客户的DataSet。
假设我们在业务层。为了简化示例,我会用硬编码实现从数据层正常获取数据。
public class BoCustomer
{
public DataSet GetCustomer(int intCustomerId)
{
// call data layer and get customer information
DataSet ds = new DataSet();
DataTable dt = new DataTable("Customer");
dt.Columns.Add("Id", typeof(int));
dt.Columns.Add("FirstName", typeof(string));
dt.Columns.Add("LastName", typeof(string));
dt.Rows.Add(intCustomerId, "Mike", "Peretz");
ds.Tables.Add(dt);
// lets make this take some time
System.Threading.Thread.Sleep(2000);
return ds;
}
}
我们创建我们的命令类,它的职责是根据客户ID更新表格。
public class GetCustomerByIdCommand : ICommand
{
private GetCustomerByIdDelegate m_invokeMe;
private DataGridView m_grid;
private int m_intCustmerId;
// notice that the delegate is private,
// only the command can use it.
private delegate DataSet GetCustomerByIdDelegate(int intCustId);
public GetCustomerByIdCommand(BoCustomer boCustomer,
DataGridView grid,
int intCustId)
{
m_grid = grid;
m_intCustmerId = intCustId;
// setup the delegate to call
m_invokeMe =
new GetCustomerByIdDelegate(boCustomer.GetCustomer);
}
public void Execute()
{
// call the method on the thread pool
m_invokeMe.BeginInvoke(m_intCustmerId,
this.CallBack, // callback!
null);
}
private void CallBack(IAsyncResult ar)
{
// get the dataset as output
DataSet ds = m_invokeMe.EndInvoke(ar);
// update the grid a thread safe fasion!
MethodInvoker updateGrid = delegate
{
m_grid.DataSource = ds.Tables[0];
};
if (m_grid.InvokeRequired)
m_grid.Invoke(updateGrid);
else
updateGrid();
}
}
注意GetCustomerByIdCommand获取了它需要执行命令的所有信息。
1、要更新的grid
2、要搜索的客户ID
3、业务层对象的引用
注意,委托隐含在命令对象的内部,因此客户无需知道命令对象内部的运作。所有的客户需要知道的是,创建命令对象以及调用Execute。目前我们知道的异步方法调用在线程池中;我们应该都知道,从线程池或任何其他不同于UI的线程更新UI是不好的!为了解决这个问题,我们在命令对象隐藏了实现,以及检查更新表格是否基于InvokeRequired为true的情况下。(InvokeRequired,该值指示调用方在对控件进行方法调用时是否必须调用 Invoke 方法,因为调用方位于创建控件所在的线程以外的线程中。From MSDN )。如果它为true,我们使用Control.Invoke确认呼叫与UI线程是一致的(注意,我们使用了.NET2.0的匿名方法特性)。我们看看表格怎么创建命令并执行的!
private ICommand m_cmdGetCustById;
private void button1_Click(object sender, EventArgs e)
{
// get the custmer id from the screen
int intCustId = Convert.ToInt32(m_txtCustId.Text);
// use the buisness layer to get the data
BoCustomer bo = new BoCustomer();
// create a command that has all the tools to update the grid
m_cmdGetCustById = new GetCustomerByIdCommand(
bo, m_grid, intCustId);
// call the command in a non blocking mode.
m_cmdGetCustById.Execute();
}
注意到了吗?执行没有被阻塞。但在你创建成千上万的命令类要发疯之前,记住以下几点。
1、命令模式会导致“类爆炸”,因此适合选用你的“模式武器”。
2、在我的例子,简单地创建命令对象,在它里面写了把在线程安全管理中更新表格的逻辑代码。然而,我只是简单化我的例子。
3、如果把TextBox对象传入命令对象也许更合理。因为可以在内部动态获取输入,并允许命令对象在任何调用的时候无需重复创建。
4、注意:委托、BeginInvoke、EndInvoke、callback方法、以及那保证UI在线程安全情况下更新的代码,全部封装在我的命令对象内。这样是非常好的!
结束
呼!写这篇文章花了我近一个礼拜时间。我尝试覆盖了所有有关在“非封闭”状态下调用方法的每个重要方面。这里有几点需要记住:
1、委托会在BeginInvoke和EndInvoke包含了正确一致的签名,你应该在调用EndInvoke时,才获取到你所有的输出参数和异常。
2、不要忘记当你使用BeginInvoke得到的好处都来自线程池,因此千万不要过分使用!
3、如果你计划使用回调,使用命令模式隐藏复杂代码也许是一个好主意。
4、个人认为,只当UI初始化的时候才阻塞UI,因此现在你应该没有任何借口!
谢谢你的阅读,祝你们每天快乐、每人从.NET获取快乐!