异步调用编程模型
为了支持异步调用多线程是必须的。但是这样可能成为系统资源的一种浪费,并且如果.NET为每个异步调用都启用一个新线程就会导致性能损失。一个更好的方式是使用一个已创建好的工作线程池。.NET恰恰有这样一个线程池,称为“.NET thread pool(.NET线程池)”。并且.NET支持异步调用的这种方式还完全隐藏了其内部实现。如前面所述,有相当多的编程模型来处理异步调用,如阻塞,等待,轮询和完成回调。一般说来,BeginInvoke()初始化一个异步方法调用。调用客户端只有短暂时刻的阻断——用来从线程池中排队请求一个线程用来执行方法,而后返回控制权给客户端。EndInvoke()管理方法完成,如接收输出参数,返回值和错误处理。
使用BeginInvoke()和EndInvoke()
编译器生成的BeginInvoke()和EndInvoke()方法如下:
public virtual IAsyncResult BeginInvoke(<intput and intput/output parameters>, AsyncCallback callback, object asyncState);
public virtual <return value> EndInvoke(<output and intput/output parameters>, IAsyncResult asyncResult);
BeginInvoke()接收原始标记的委托定义中的输入参数。输入参数包括通过值或引用(使用out或ref修饰符)传入的值类型和引用类型。原始方法的返回值和任何显式的输出参数组成EndInvoke()方法。如这样一个委托定义:
public delegate string MyDelegate(int number1, out int number2, ref int number3, object obj);
//相应的BeginInvoke()和EndInvoke()
public virtual IAsyncResult BeginInvoke(int number1, out int number2, ref int number3, object obj);
public virtual string EndInvoke(out int number2, ref int number3, IAsyncResult asyncResult);
BeginInvoke()还可以接收两个额外没有出现在原始委托标记中的输入参数:AsyncCallback callback和object asyncState。callback参数实际上是一个代表回调方法引用的一个委托对象,用来接收方法完成的通知事件。asyncState是一个处理方法完成时所需何种状态信息而传入的一个泛型对象。这两个参数都是可选的:调用者可以选择传入null来取代任何一个参数。例如,异步调用Calculator类中的Add()方法,如果对于结果和回调方法或状态信息不感兴趣,可以这样:
Calculator calculator = new Calculator();
BinaryOperation oppDel = calculator.Add;
oppDel.BeginInvoke(2, 3, null, null);
对象自身并不知道客户端代码使用委托来异步调用方法。同一对象代码都可以处理同步和异步调用的情况。因此,每一个.NET对象都支持异步调用。但仍需注意遵守前面提到的设计指南,即使类可以通过编译。
因为委托就可以用于实例方法,也可以用于静态方法,所以客户端同样可以异步调用静态方法。
IAsyncResult接口
每个BeginInvoke()方法都返回一个实现IAsyncResult接口的对象,该接口定义如下:
public interface IAsyncResult
{
object AsyncState { get;}
WaitHandle AsyncWaitHandle { get;}
bool CompletedSynchronously { get;}
bool IsCompleted { get;}
}
返回的实现IAsyncResult的对象用于惟一标识使用BeginInvoke()调用的方法。可以在EndInvoke()方法中传入IAsyncResult对象,标识特定的异步方法执行。例如:
Calculator calculator = new Calculator();
BinaryOperation oppDel = calculator.Add;
IAsyncResult asyncResult1 = oppDel.BeginInvoke(2, 3, null, null);
IAsyncResult asyncResult2 = oppDel.BeginInvoke(4, 5, null, null);
/* 做一些工作 */
int result;
result = oppDel.EndInvoke(asyncResult1);
Debug.Assert(result == 5);
result = oppDel.EndInvoke(asyncResult2);
Debug.Assert(result == 9);
上面的示例中展示了一些关键点。第一个关键点是EndInvoke()方法的主要作用是取回输出参数以及方法的返回值,EndInvoke()会阻断它的调用者直到等待的方法返回为止。第二个关键点是同一委托对象可以调用目标方法上的多个异步请求。调用者可以使用BeginInvoke()返回的惟一AsyncResult对象标识,来辨识多个挂起的调用。事实上,当调用者发起异步调用时,调用者必须保存IAsyncResult对象。此外,调用者对于挂起的调用的完成次序不应存在假设。记住:异步调用是在来源于线程池中的线程上执行的,并且由于线程的上下文切换,是十分有可能产出第二个调用先与第一个调用完成的情况。
另外,通过把IAsyncResult对象传入EndInvoke()还有其他用途:可以使用它来获得BeginInvoke()的状态对象参数,可以等待方法完成,并获得原始的用于调用请求的委托。当使用基于委托的异步调用时,需要记住以下三个十分重要的编程要点:
1.EndInvoke()只能在每个异步操作中调用一次。如果试图多次调用会导致InvalidOperationException类型的异常。
2.虽然普遍情况下编译器生成的委托类可以管理多个目标方法,但当使用异步方式调用委托时仅仅允许调用内部列表中确切的一个目标方法。当委托列表中包含一个以上的目标方法的时候,调用BeginInvoke()会导致ArgumentException异常,报告委托必须只有一个目标方法。
3.只有在同一用于调度调用的委托上,才可以传入IAsyncResult对象到EndInvoke()方法。传入IAsyncResult对象到一个不同的委托会导致InvalidOperationException异常,报告“The IAsyncResult object provided doesn't match this delegate(提供的IAsyncResult对象与该委托不匹配)”,即使其他委托指向同一个方法:
Calculator calculator = new Calculator();
BinaryOperation oppDel1 = calculator.Add;
BinaryOperation oppDel2 = calculator.Add;
IAsyncResult asyncResult = oppDel1.BeginInvoke(2, 3, null, null);
//引发InvalidOperationException异常
oppDel2.EndInvoke(asyncResult);
AsyncResult类
通常一个客户端启动一个异步回调,但另一个却调用了EndInvoke()方法。即使只涉及到一个客户端,也很可能在一组代码(或方法)中调用BeginInvoke(),而在另一部分中调用EndInvoke()。这样就不得不保存IAsyncResult对象或者是传递它到另一个客户端,这些都是不好的做法。更糟的是,不得不对调用异步回调的委托做同样的工作,因为需要委托来调用EndInvoke()。值得庆幸的是,有一个比较简单的解决方法可以使用,因为IAsyncResult对象本身持有所创建的委托。当BeginInvoke()返回IAsyncResult引用时,它实际是一个AsyncResult类的实例,定义如下:
public class AysncResult : IAsyncResult, IMessageSink
{
//IAsyncResult实现
public virtual object AsyncResult { get;}
public virtual WaitHandle AsyncWaitHandle { get;}
public virtual bool CompletedSynchronously { get;}
public virtual bool IsCompleted { get;}
//其他属性
public bool EndInvokeCalled { get;}
public virtual object AsyncDelegate { get;}
//IMessageSink实现
}
AsyncResult对象定义在System.Runtime.Remoting.Messaging命名空间下。AsyncResult有一个称为AsyncResult的属性,就是原始调度调用的委托引用。下面的示例就展示了如何使用AsyncDelegate属性调用EndInvoke()上的原始委托:
public class CalculatorClient
{
IAsyncResult m_AsyncResult;
public void AsyncAdd()
{
Calculator calculator = new Calculator();
DispatchAdd(calculator, 2, 3);
/* 做一些工作*/
int result = GetResult();
Debug.Assert(result == 5);
}
void DispatchAdd(Calculator calculator, int number1, int number2)
{
BinaryOperation oppDel = calculator.Add;
m_AsyncResult = oppDel.BeginInvoke(2, 3, null, null);
}
int GetResult()
{
int result = 0;
//获得原始委托
AsyncResult asyncResult = (AsyncResult)m_AsyncResult;
BinaryOperation oppDel = (BinaryOperation)asyncResult.AsyncDelegate;
Debug.Assert(asyncResult.EndInvokeCalled == false);
result = oppDel.EndInvoke(m_AsyncResult);
return result;
}
}
注意,因为AsyncResult是对象类型,需要传递它到实际的委托类型。上面的示例中还展示了使用AsyncResult另一个十分有用的属性——bool类型的EndInvokeCalled,可以使用它来验证EndInvoke()方法是否已经被调用:
Debug.Assert(asyncResult.EndInvokeCalled == false);
轮询或等待完成
当一个客户端调用EndInvoke()时,该客户端会被阻断直到异步调用返回为止。如果在调用执行期间客户端只有有限的工作要处理,这样是可以接收的,并且如果这些工作完成后,客户端不可以在没有异步调用方法的返回值或输出参数的情况下继续执行。但是,如果客户端只想检查方法是否执行完毕那?或客户端只想以固定的时间等待完成,然后做一些额外的处理,然后再返回等待?.NET当然支持这些选择性的编程模型。
从BeginInvoke()返回的IAsyncResult接口对象拥有一个类型为WaitHandle的AsyncWaitHandle属性。WaitHandle实际上是一个本地Windows可等待事件句柄的封装。WaitHandle包含一些重载等待方法。例如,WaitOne()方法只在句柄为单个的时候返回:
Calculator calculator = new Calculator();
BinaryOperation oppDel = calculator.Add;
IAsyncResult asyncResult = oppDel.BeginInvoke(2, 3, null, null);
/* 做一些工作 */
asyncResult.AsyncWaitHandle.WaitOne(); //可能阻塞
int result;
result = oppDel.EndInvoke(asyncResult); //不会阻塞
Debug.Assert(result == 5);
如果当WaitOne()被调用时,如果异步方法仍在执行,则它会阻塞。如果方法已经完成,则WaitOne()不会阻塞并且客户端会继续为返回值调用EndInvoke()。与之前的例子中调用EndInvoke()不同的是,上面例子中的方式保证不会阻塞其调用者。下面的例子展示了一种更为具体的通过指定时间片段来使用WaitOne()的方式。当指定了一个时间片段后,WaitOne()会在方法执行完成或时间片段经过后返回:
Calculator calculator = new Calculator();
BinaryOperation oppDel = calculator.Add;
IAsyncResult asyncResult = oppDel.BeginInvoke(2, 3, null, null);
while (asyncResult.IsCompleted == false)
{
asyncResult.AsyncWaitHandle.WaitOne(10, false); //可能阻塞
/* 做一些工作 */
}
int result;
result = oppDel.EndInvoke(asyncResult); //不会阻塞
上面的示例上也使用了IAsyncResult的IsCompleted属性。IsCompleted可以不用通过等待或阻塞的方式来得到调用的状态。因此,可以在严格的轮询模式中使用IsCompleted属性:
while (asyncResult.IsCompleted == false)
{
/* 做一些工作 */
}
当然,这样也存在轮询机制所带来的所有不利影响,所以应尽量避免以这种方式来使用IsCompleted属性。
AsyncWaitHandle真正的亮点是使用它来管理多个当前处理中的异步方法。可以使用WaitHandle类的静态WaitAll()方法来等待多个异步方法的完成:
Calculator calculator = new Calculator();
BinaryOperation oppDel1 = calculator.Add;
BinaryOperation oppDel2 = calculator.Add;
IAsyncResult asyncResult1 = oppDel1.BeginInvoke(2, 3, null, null);
IAsyncResult asyncResult2 = oppDel1.BeginInvoke(4, 5, null, null);
WaitHandle[] handleArray ={ asyncResult1.AsyncWaitHandle, asyncResult2.AsyncWaitHandle };
WaitHandle.WaitAll(handleArray);
int result;
//这些对EndInvoke()的调用不会阻塞
result = oppDel1.EndInvoke(asyncResult1);
Debug.Assert(result == 5);
result = oppDel2.EndInvoke(asyncResult2);
Debug.Assert(result == 9);
为了使用WaitAll(),就需要构造一个句柄数组。注意,同样地仍要调用EndInvoke()方法来访问返回值。
为了取代等待所有方法返回,可以选择使用等待其中的任意个,通过使用WaitHandle中的WaitAny()静态方法:
WaitHandle.WaitAny(handleArray);
类似于WaitOne(),WaitAll()和WaitAny()都有可以指定等待时间片段的重载版本来取代不确定的等待。
使用完成回调方法
与可选择性地管理异步调用一样,.NET一起提供了另一个编程模型:callback(回调)。概念十分简单:客户端提供给.NET一个方法,并要求.NET在异步方法完成时回来调用这个方法。客户端可以提供一个回调实例方法或者静态方法,并且可以使用同一回调方法处理多个异步方法的完成。唯一的要求是回调方法须符合以下签名,对于回调方法的命名约定是以On为前缀——例如OnAsyncCallBack(),OnMethodCompletion()等等:
<visibility modifier> void <Name>(IAsyncResult asyncResult);
.NET使用来自线程池中的一个线程,通过BeginInvoke()方法来执行方法调度。当异步方法执行完成时,该工作线程会调用回调方法,而不是悄悄返回到线程池中。为了使用回调方法,客户端需要提供一个具有指向回调方法委托的BeginInvoke()。该委托是以参数的形式提供给BeginInvoke(),并且总是AsyncCallback类型。AsyncCallback是.NET提供的System命名空间下的委托,定义如下:
public delegate void AsyncCallback(IAsyncResult asyncResult);
当为BeginInvoke()提供完成回调方法时,可以依赖于委托引用,也可以直接传入方法名称,下面的示例中展示了使用一个完成回调方法进行的异步调用管理:
public class CalculatorClient
{
public void AsyncAdd()
{
Calculator calculator = new Calculator();
BinaryOperation oppDel = calculator.Add;
oppDel.BeginInvoke(2, 3, OnMethodCompletion, null);
}
void OnMethodCompletion(IAsyncResult asyncResult)
{
int result = 0;
AsyncResult resultObj = (AsyncResult)asyncResult;
Debug.Assert(resultObj.EndInvokeCalled == false);
BinaryOperation oppDel = (BinaryOperation)resultObj.AsyncDelegate;
result = oppDel.EndInvoke(asyncResult);
Trace.WriteLine("Operation returned" + result);
}
}
与前面提到的编程模型不同的是,当使用一个完成回调方法时,就没有必要保存返回自BeginInvoke()的IAsyncResult对象——当.NET调用回调方法时,它会提供IAsyncResult对象作为参数。注意上面示例中通过把IAsyncResult对象参数转换为AsyncResult对象,来获得用于调度调用的原始委托,并使用这个委托来调用EndInvoke()。因为.NET为每一个异步方法都提供一个IAsyncResult对象,并可以将多个异步方法完成导向同一个回调方法:
Calculator calculator = new Calculator();
BinaryOperation oppDel1 = calculator.Add;
BinaryOperation oppDel2 = calculator.Add;
oppDel1.BeginInvoke(2, 3, OnMethodCompletion, null);
oppDel2.BeginInvoke(4, 5, OnMethodCompletion, null);
完成回调方法显然是任何事件驱动应用程序中的首选模型。一个事件驱动应用程序中拥有触发事件的方法(或者调度请求和传递处理消息),以及处理这些请求并作为结果激发它们自己的事件的方法。以事件驱动来编写应用程序,会更便于管理多线程,事件和消息,并可以得到更好的可靠性,应答性和性能。.NET使用完成回调方法的异步调用管理像手套套在手上一样融合为一个整体构架。而其他方式(等待,阻塞和轮询)对于在其执行流程中有严格,明确和确定定义的应用程序中也是可用的。
建议,尽可能的应用程序使用完成回调方法。
回调方法和线程安全
因为回调方法是在来自线程池中的某一线程中执行,这样就必须在回调方法和提供它的对象中提供线程安全。这就意味着需要使用同步对象或锁来访问对象成员变量。并小心对象的“正常”线程和来自线程池的线程之间的同步,还有潜在的多个同时调用回调方法来处理各自异步方法完成的工作线程之间的同步。回调方法必须保证可重入和线程安全。
传递状态信息
BeginInvoke()的最后一个参数,object asyncState,作为.NET中的作为泛用容器被提供的状态对象。处理方法完成的部分可以访问类似于IAsyncResult中AsyncState属性的容器对象。虽然也可以在其他.NET异步调用编程模型(阻塞,等待或轮询)中使用状态对象,但与完成方法一起使用时最有用处。原因很简单:在其他编程模型中,不仅必须管理IAsyncResult对象,而且还要管理一个额外的增加管理责任的容器对象。当通过完成回调使用容器对象时,仅需要给回调方法传入一个额外的参数,而且已通过.NET在方法签名中预订了。
下面的示例中展示了如何使用状态对象把一个整数值作为一个额外参数传入完成回调方法中。注意,根据Debug.Assert()的要求必须转换AsyncState属性到实际的类型:
public class CalculatorClient
{
public void AsyncAdd()
{
Calculator calculator = new Calculator();
BinaryOperation oppDel = calculator.Add;
int asyncState = 4;
oppDel.BeginInvoke(2, 3, OnMethodCompletion, asyncState);
}
void OnMethodCompletion(IAsyncResult asyncResult)
{
int asyncState;
asyncState = (int)asyncResult.AsyncState;
Debug.Assert(asyncState == 4);
/* 回调的其余部分*/
}
}
不使用委托执行异步操作
基于委托的异步调用可以在任何类中异步调用方法。这项技术为、提供给客户端十分有价值的灵活性,但是它要求定义一个与想要调用的方法的签名匹配的委托。某些操作,如磁盘或网络访问,Web请求,Web Service调用,提交转换或消息队列,都会消耗大量时间。在这些情况下,通常会选择异步调用这些操作。.Net Framework的设计者们设法通过内建到提供Begin<Operation>和End<Operation>方法的类中,来缓和这些操作的执行任务。这些方法以与BeginInvoke()和EndInvoke()完全相同的方式通过委托类来提供:
public <return type> <Operation>(<parameters>);
IAsyncResult Begin<Operation>(<intput and input/output parameters>, AsyncCallback callback, object asyncState);
public <return type> End<Operation>(<output and intput/output parameters>, IAsyncResult asyncResult);
例如,定义于System.IO命名空间下的抽象类Stream,就提供了异步Read()和Write()操作:
public abstract class Stream : MarshalByRefObject, IDisposable
{
public virtual int Read(byte[] buffer, int offset, int count);
public virtual IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state);
public virtual int EndRead(IAsyncResult asyncResult);
public virtual void Write(byte[] buffer, int offset, int count);
public virtual IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state);
public virtual void EndWrite(IAsyncResult asyncResult);
/* 其他方法和属性 */
}
Stream类是其他所有stream类的基类,如FileStream,MemoryStream和NetworkStream。所有派生类都会重写这些方法,并提供自己的实现。下面的示例中展示在一个FileStream对象上的异步读取操作。注意useAsync参数传入FileStream构造器,指示了流上的异步操作:
public class FileStreamClient
{
byte[] m_Array = new Byte[2000];
public void AsyncRead()
{
bool useAsync = true;
Stream stream = new FileStream("MyFile.bin", FileMode.Open, FileAccess.Read, FileShare.None, 1000, useAsync);
using (stream)
{
stream.BeginRead(m_Array, 0, 10, OnMethodCompletion, null);
}
}
void OnMethodCompletion(IAsyncResult asyncResult)
{
bool useAsync = true;
Stream stream = new FileStream("MyFile.bin", FileMode.Open, FileAccess.Read, FileShare.None, 1000, useAsync);
using (stream)
{
int bytesRead = stream.EndRead(asyncResult);
}
//访问m_Array
}
}
提供自己的异步方法的类的另一个例子是可以使用WSDL.exe命令提示符工具生成的服务代理类。想象一下前面提到的Calculator类作为Web Service来暴露它的方法:
public class Calculator : System.Web.Services.WebService
{
[WebMethod]
public int Add(int argument1, int argument2)
{
argument1 + argument2;
}
}
WSDL.exe为客户端自动生成的代理类会包含异步调用Web Service的BeginAdd()和EndAdd()方法:
public partial class Calculator : SoapHttpClientProtocol
{
public int Add(int argument1, int argument2)
{... }
public IAsyncResult BeginAdd(int argument1, int argument2, AsyncCallback callback, object asyncState)
{... }
public int EndAdd(IAsyncResult asyncResult)
{... }
/* 其他成员 */
}
使用非基于委托的异步方法调用类似于通过委托类提供的BeginInvoke()和EndInvoke():使用Begin<Operation>调度异步操作,在完成之前调用End<Operation>阻塞,等待操作(或多个操作)完成,或者是使用一个回调方法。但是,对于用于调度Begin<Operation>的原始对象上的End<Operation>的调用,却没有统一的要求。在有些类中(如Web Service代理类或Stream派生类),可以创建一个新类并调用其上的End<Operation>。下面的示例展示了当使用Web Service代理类时,这项技术的应用:
public class CalculatorWebServiceClient
{
public void AsyncAdd()
{
//Calculator是WSDL.exe生成的代理类
Calculator calculator = new Calculator();
calculator.BeginAdd(2, 3, OnMethodCompletion, null);
}
void OnMethodCompletion(IAsyncResult asyncResult)
{
//Calculator是WSDL.exe生成的代理类
Calculator calculator = new Calculator();
int result;
result = calculator.EndAdd(asyncResult);
Trace.WriteLine("Operation returned " + result);
}
}
根据原版英文翻译,所以不足和错误之处请大家不吝指正,谢谢:)