在上一篇文章中,我们探讨了使用Thread类实现异步的方法。
在整个过程中,可以发现Delegate这个东西出现了很多次。而仔细研究Delegate,我们发现每一个Delegate类型都自动产生了Invoke、BeginInvoke、EndInvoke等方法。而BeginInvoke、EndInvoke这两个方法,我们马上就可以猜到这是用来实现异步的~~
那么我们现在就看一下怎样使用委托来实现异步。
Delegate的BeginInvoke、EndInvoke两个方法,是编译器自动生成的,专门用来实现异步,这里是MSDN中关于这两个方法的说明:
异步委托提供以异步方式调用同步方法的能力。当同步调用一个委托时,“Invoke”方法直接对当前线程调用目标方法。如果编译器支持异步委托,则它将生成“Invoke”方法以及“BeginInvoke”和“EndInvoke”方法。如果调用“BeginInvoke”方法,则公共语言运行库 (CLR) 将对请求进行排队并立即返回到调用方。将对来自线程池的线程调用该目标方法。提交请求的原始线程自由地继续与目标方法并行执行,该目标方法是对线程池线程运行的。如果在对“BeginInvoke”方法的调用中指定了回调方法,则当目标方法返回时将调用该回调方法。在回调方法中,“EndInvoke”方法获取返回值和所有输入/输出参数。如果在调用“BeginInvoke”时未指定任何回调方法,则可以从调用“BeginInvoke”的线程中调用“EndInvoke”。
其中,BeginInvoke用来启动异步,与Thread类不同的是这里的异步使用CLR管理的。BeginInvoke方法的最后两个参数总是一个AsyncCallback委托对象和一个object类型,其中AsyncCallback委托就是当异步执行完成时将要被调用的函数入口,也就是上一篇中用来实现“在异步完成时通知我”这个功能的。而最后一个object类型,则是用来传递参数的,其实与上一篇中ParameterizedThreadStart委托的参数是类似的——不过他们还是有着明显的区别:使用ParameterizedThreadStart委托时永远只能接受一个object类型的参数,因此如果原本要异步执行的函数具有多个参数,必须进行封装;而使用BeginInvoke方法则不同,编译器生成的BeginInvoke方法前面几个参数(除了最后两个)的类型跟声明委托时的参数个数和类型完全相同,这样就不必再封装参数了,最后一个object参数只是一个补充的参数,一般情况下是不需要的:
private
void
DoMain(
string
cmd,
string
[] args)
{
SumDelegate handle = new SumDelegate(this.Sum);
IAsyncResult ar = handle.BeginInvoke(1, 2, null, null);
}
public
delegate
int
SumDelegate(
int
x,
int
y);
public
int
Sum(
int
x,
int
y)
{
return x + y;
}
我们可以看到,在调用BeginInvoke的时候,方法的后面两个参数就是对应的AsyncCallback和object参数,这里因为我们没有用到这个回调和参数,就都传递了null;而BeginInvoke的前面两个方法,就对应的是Sum函数的两个参数x和y。因此,这个BeginInvoke方法还在代码编译的时候就帮我们检查了函数的输入参数个数以及类型。
当使用Thread类时,我们可以通过判断Thread类的ThreadStatus来判断线程是否已经执行结束。而如果用Delegate.BeginInvoke方法,我们则需要根据其返回的一个
IAsyncResult对象的IsCompleted属性来获取“异步操作是否已完成的指示”:当这个属性变成True时,就表示异步已经执行结束:
private
void
DoMain(
string
cmd,
string
[] args)
{
SumDelegate handle = new SumDelegate(this.Sum);
IAsyncResult ar = handle.BeginInvoke(1, 2, null, null);
while(!ar.IsCompleted)
{
Thread.Sleep(10);
}
// 异步已经执行完毕
}
public
delegate
int
SumDelegate(
int
x,
int
y);
public
int
Sum(
int
x,
int
y)
{
return x + y;
}
当然,前面我们提到,BeginInvoke方法总是会接收一个AsyncCallback类型的委托,当异步执行完毕后,CLR就会自动调用这个委托封装的函数。因此,我们还可以通过这个委托来接受异步已经完成的通知:
private
void
DoMain(
string
cmd,
string
[] args)
{
SumDelegate handle = new SumDelegate(this.Sum);
AsyncCallback callback = new AsyncCallback(this.OnSumCompleted);
IAsyncResult ar = handle.BeginInvoke(1, 2, callback, null);
}
public
delegate
int
SumDelegate(
int
x,
int
y);
public
int
Sum(
int
x,
int
y)
{
return x + y;
}
public
void
OnSumCompleted(IAsyncResult ar)
{
// 异步已经执行完毕
Debug.Assert(ar.IsCompleted);
}
注意这里,当向BeginInvoke传入的AsyncCallback被执行时,IAsyncResult对象的IsCompleted属性一定是True。另外,BeginInvoke方法传递的最后一个object参数,实际上就是保存在了IAsyncResult的AsyncState属性中。
上面已经提到了两种等待异步调用执行完毕的方法:主动轮询 和 异步执行完毕时执行回调方法。除了这两种方法,我们还可以通过EndInvoke方法来直接阻塞线程(并不是每次都会阻塞,这个我们下面再讲)直到异步执行完成:
private
void
DoMain(
string
cmd,
string
[] args)
{
SumDelegate handle = new SumDelegate(this.Sum);
AsyncCallback callback = new AsyncCallback(this.OnSumCompleted);
IAsyncResult ar = handle.BeginInvoke(1, 2, null, null);
int value = handle.EndInvoke(ar);
Debug.Assert(value == 3);
}
public
delegate
int
SumDelegate(
int
x,
int
y);
public
int
Sum(
int
x,
int
y)
{
return x + y;
}
当调用EndInvoke时,必须把BeginInvoke返回的IAsyncResult对象作为参数传递,这样EndInvoke才可以通过IAsyncResult对象得知要等待哪个方法异步执行完毕。因为在BeginInvoke返回的IAsyncResult中,属性AsyncWaitHandle指示了用于等待异步执行完毕的一个句柄。如果你调用了很多次BeginInvoke,就会启动很多个异步任务,每次调用返回的IAsyncResult就会对应的保存了不同的句柄。另外,这里可以看到,EndInvoke方法的返回结果,实际上就是我们在定义
SumDelegate委托时声明的返回值类型,这个也是编译器自动帮我们生成的。
那么,我们刚才提到EndInvoke方法“并不是每次都会阻塞”。为什么呢?原因很简单:在EndInvoke方法内部,首先会判断IAsyncResult.IsCompleted属性,如果为True,则直接返回执行结果,否则调用AsyncWaitHandle这个句柄的
WaitOne方法,这个方法“阻止当前线程,直到当前的 WaitHandle 收到异步调用已经结束的信号”,然后返回执行结果。
因此,与之对应的,我们还有另外一个方法来等待异步执行结束,那就是我们直接访问AsyncWaitHandle:
private
void
DoMain(
string
cmd,
string
[] args)
{
SumDelegate handle = new SumDelegate(this.Sum);
AsyncCallback callback = new AsyncCallback(this.OnSumCompleted);
IAsyncResult ar = handle.BeginInvoke(1, 2, null, null);
if(!ar.IsCompleted)
{
ar.AsyncWaitHandle.WaitOne();
}
// 异步调用已结束。
Debug.Assert(ar.IsCompleted);
}
public
delegate
int
SumDelegate(
int
x,
int
y);
public
int
Sum(
int
x,
int
y)
{
return x + y;
}
实际上,这个方式跟EndInvoke是完全相同的。
这下我们应该明白刚才所说的“并不是每次都会阻塞”了吧?没错:当ar.IsCompleted为True时,就会直接返回函数执行结果,否则才会调用WaitHandle的WaitOne来阻塞线程。
通过Delegate对象,我们可以使得我们的类更方便的支持异步方法。就好像刚才的类里面,我们有个Sum方法,然后通过定义一个可以接受这个函数的Delegate,然后用户就可以使用这个Delegate、AsyncCallback、IAsyncResult等对象来实现异步了。
那么我们可不可以为客户封装的更简单一点呢?就好像FileStream类,就有Read、BeginRead、EndRead三个方法,非常简单好用。很明显的,FileStream对象是封装了对Delegate对象的BeginInvoke、EndInvoke方法的调用。那么我们怎样去实现这样的效果呢?
下面,我们利用实现一个支持异步调用的一个类,这个类有个用于同步执行的Sum函数,和一个异步执行的BeginSum、EndSum函数:
public
class
MyClass1
{
private delegate int SumDelegate(int a, int b);
private SumDelegate _sumHandler;
public MyClass1()
{
this._sumHandler = new SumDelegate(this.Sum);
}
public int Sum(int a, int b)
{
return a + b;
}
public IAsyncResult BeginSum(int a, int b, AsyncCallback callback, object stateObject)
{
return this._sumHandler.BeginInvoke(a, b, callback, stateObject);
}
public int EndSum(IAsyncResult asyncResult)
{
return this._sumHandler.EndInvoke(asyncResult);
}
}
注意这个类的内部,声明了一个私有的委托类型“SumDelegate”,以及一个类型为SumDelegate的私有变量。我们把对这个委托的BeginInvoke、EndInvoke的调用,分别封装在了BeginSum、EndSum中。这样,用户在异步调用Sum方法时,就不用为了封装Sum函数而声明一个新的委托了。