现在的位置: 首页 > 综合 > 正文

《Programming .Net Components》学习笔记(十七)

2011年06月02日 ⁄ 综合 ⁄ 共 8026字 ⁄ 字号 评论关闭

异步调用编程模型

为了支持异步调用多线程是必须的。但是这样可能成为系统资源的一种浪费,并且如果.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()提供完成回调方法时,可以依赖于委托引用,也可以直接传入方法名称,下面的示例中展示了使用一个完成回调方法进行的异步调用管理:

抱歉!评论已关闭.