使用IAsyncResult接口实现异步调用

使用IAsyncResult接口实现异步调用

在.net framework中,许多I/O操作(文件I/O操作以及网络I/O)都提供异步版本的API,我们可以直接使用这些API来达到异步调用的目的。 在今天的示例中,发送HTTP请求的API中,就支持异步操作,我将演示使用这些异步API的操作过程。

在客户端,我将使用以下代码完成异步调用过程:

/// 
/// 使用IAsyncResult接口实现异步调用
/// 
/// 
private void CallViaIAsyncResult(string str)
{
    HttpWebRequestHelper.SendHttpRequestAsync(ServiceUrl, str, CallViaIAsyncResultCallback, null);
}

private void CallViaIAsyncResultCallback(string str, string result, Exception ex, object state)
{
    if( ex == null )
        ShowResult(string.Format("{0} => {1}", str, result));
    else
        ShowResult(string.Format("{0} => Error: {1}", str, ex.Message));
}

其中HttpWebRequestHelper.SendHttpRequestAsync()是个简单的包装方法,最终异步操作的实现代码如下:

/// 
/// 用于所有回调状态的数据类
/// 
private class MyCallbackParam
{
    public TIn InputData;
    public Action<TIn, TOut, Exception, object> Callback;
    public object State;
    public HttpWebRequest Request;
    public JavaScriptSerializer Jss;
}

/// 
/// 异步调用服务
/// 
/// 
/// 
/// 服务调用完成后的回调委托,用于处理调用结果
/// 
public static void SendHttpRequestAsync(string url, TIn input, 
                Action<TIn, TOut, Exception, object> callback, object state)
{
    if( string.IsNullOrEmpty(url) )
        throw new ArgumentNullException("url");
    if( input == null )
        throw new ArgumentNullException("input");
    if( callback == null )
        throw new ArgumentNullException("callback");

    // 创建请求对象
    HttpWebRequest request = CreateHttpWebRequest(url, "json");

    // 记录必要的回调参数
    MyCallbackParam cp = new MyCallbackParam {
        Callback = callback,
        InputData = input,
        State = state,
        Request = request,
    };

    // 开始异步写入请求数据
    request.BeginGetRequestStream(AsyncWriteRequestStream, cp);

    // 虽然BeginGetRequestStream()可以返回一个IAsyncResult对象,
    // 但我却不想返回这个对象,因为整个过程需要二次异步。
}

private static void AsyncWriteRequestStream(IAsyncResult ar)
{
    // 取出回调前的状态参数
    MyCallbackParam cp = (MyCallbackParam)ar.AsyncState;

    try {
        // 为了简单,这里仅使用JSON序列化方式
        JavaScriptSerializer jss = new JavaScriptSerializer();
        string jsonData = jss.Serialize(cp.InputData);
        cp.Jss = jss;

        // 结束写入数据的操作
        using( BinaryWriter bw = new BinaryWriter(cp.Request.EndGetRequestStream(ar)) ) {
            bw.Write(DefaultEncoding.GetBytes(jsonData));
        }

        // 开始异步向服务器发起请求
        cp.Request.BeginGetResponse(GetResponseCallback, cp);
    }
    catch( Exception ex ) {
        cp.Callback(cp.InputData, default(TOut), ex, cp.State);
    }
}

private static void GetResponseCallback(IAsyncResult ar)
{
    // 取出回调前的状态参数
    MyCallbackParam cp = (MyCallbackParam)ar.AsyncState;

    try {
        // 读取服务器的响应
        using( HttpWebResponse response = (HttpWebResponse)cp.Request.EndGetResponse(ar) ) {
            string responseText = ReadResponse(response);
            TOut result = cp.Jss.Deserialize<TOut>(responseText);

            // 返回结果,通过回调用户的回调方法来完成。
            cp.Callback(cp.InputData, result, null, cp.State);
        }
    }
    catch( Exception ex ) {
        cp.Callback(cp.InputData, default(TOut), ex, cp.State);
    }
}

注意:在SendHttpRequestAsync方法的实现过程中,需要发起二次异步调用:BeginGetRequestStream, BeginGetResponse 。自然地, 也会引起二次回调,二次EndXXXXX()方法的调用。为了能在回调过程中,维持一些必要的状态参数,我定义了一个私有类型MyCallbackParam , 它包含了所有回调过程中所需要的中间状态。这里尤其要注意的是:如果某个异步操作过程需要多次异步调用,那么每个步骤都要求是异步的, 也就是要【一路异步到底】。如果中间任何一个步骤不是异步调用的,那么整个过程将不会是异步的,甚至某些API的设计者会抛出一个异常,这也是有可能的。 为了支持异步,我的包装方法也是通过回调的方式来设计的。这些都是异步设计的关键。

当某个异步操作过程需要多次调用时,该如何知道哪些步骤必须以异步形式调用呢? 比如,前面演示的发送HTTP请求的过程,我该如何知道要调用BeginGetRequestStream,BeginGetResponse这二个异步方法呢? 对于这个问题,没有一个明确的答案,因为在这方面,并没有一个规范或者约定,要根据相应组件的具体实现过程而定。 不过,通常每个支持异步组件所提供的API接口都会以BeginXxxxxx,EndXxxxxx的形式表示支持异步操作,并提供一个Xxxxxx的同步版本。 这里我可以提供一个小经验:逐个将一些关键步骤的同步调用替换成异步调用,直到实现异步过程为止。 再补充一句:对于微软提供的组件,查阅MSDN对于该方法的说明一般是可以找到线索的。

或许有些人不想定义这些回调方法,以及用于维护回调的状态类型,而选择闭包的方式。这种方法就技术的实现而言,也是可行的。 这里我就不演示了,因为我不喜欢搞大的闭包。 如果您喜欢闭包的方式,也请不要批我,每个人有每个人的喜好。

异常的处理:与委托的异步调用一样,此时也只能在调用EndXxxxxx时捕获异常。 不过,对于一个有着多个异步调用步骤的过程来说,异常的处理将要分阶段处理。

与委托异步调用的差别: 由于委托的BeginInvoke调用也能返回IAsyncResult,因此前面演示的委托的【同步并行执行】方式也可以在BeginXxxxxx/EndXxxxxx所支持的过程中使用。 但本小节所说的异步与委托的异步还是有差别,最重要的差别在于委托的异步调用阻塞发生在线程池的工作线程, 而直接使用基于IAsyncResult的异步,阻塞发生在线程池的I/O完成线程。这二种不同的线程对于不同的编程模型来说,意义是非常重大的。

小结:如果某个组件提供BeginXxxxxx/EndXxxxxx方法,通常表示可以支持异步操作,只要我们正确地调用它们就可以实现异步。

你可能感兴趣的:(C#)