技术开发 频道

ASP.NET的各种异步操作实现详细解析

  ASP.NET MVC 中的异步方式

  在ASP.NET MVC框架中,感觉一下回到原始社会中,简直和异步页的封装没法比。来看代码吧。(注意代码中的注释)

// 实际可处理的Action名称为 Test1 ,注意名称后要加上 Async
public void Test1Async()
{
    
// 告诉ASP.NET MVC,要开始一个异步操作了。
    AsyncManager.OutstandingOperations.Increment();

    
string str = Guid.NewGuid().ToString();
    MyAysncClient
<string, string> client = new MyAysncClient<string, string>(ServiceUrl);
    client.OnCallCompleted
+= new MyAysncClient<string, string>.CallCompletedEventHandler(client_OnCallCompleted);
    client.CallAysnc(
str, str);        // 开始异步调用

}

void client_OnCallCompleted(
object sender, MyAysncClient<string, string>.CallCompletedEventArgs e)
{
    
// 告诉ASP.NET MVC,一个异步操作结束了。
    AsyncManager.OutstandingOperations.Decrement();

    
if( e.Error == null )
        AsyncManager.Parameters[
"result"] = string.Format("{0} => {1}", e.UserState, e.Result);
    
else
        AsyncManager.Parameters[
"result"] = string.Format("{0} => Error: {1}", e.UserState, e.Error.Message);

    
// AsyncManager.Parameters["result"] 用于写输出结果。
    
// 这里仍然采用类似ViewData的设计。
    
// 注意:key 的名称要和Test1Completed的参数名匹配。
}

// 注意名称后要加上 Completed ,且其余部分与Test1Async的前缀对应。
public ActionResult Test1Completed(string result)
{
    ViewData[
"result"] = result;
    
return View();
}

 

  说明:如果您认为单独为事件处理器写个方法看起来不爽,您也可以采用匿名委托之类的闭包写法,这个纯属个人喜好问题。

  再来个多次异步操作的示例:

public void Test2Async()
{
    
// 表示要开启3个异步操作。
    
// 如果把这个数字设为2,极有可能会产生的错误的结果。不信您可以试一下。
    AsyncManager.OutstandingOperations.Increment(
3);

    
string str = Guid.NewGuid().ToString();
    MyAysncClient
<string, string> client = new MyAysncClient<string, string>(ServiceUrl);
    client.UserData
= "result1";
    client.OnCallCompleted
+= new MyAysncClient<string, string>.CallCompletedEventHandler(client2_OnCallCompleted);
    client.CallAysnc(
str, str);        // 开始第一个异步任务

    
string str2 = "T2_" + Guid.NewGuid().ToString();
    MyAysncClient
<string, string> client2 = new MyAysncClient<string, string>(ServiceUrl);
    client2.UserData
= "result2";
    client2.OnCallCompleted
+= new MyAysncClient<string, string>.CallCompletedEventHandler(client2_OnCallCompleted);
    client2.CallAysnc(str2, str2);        
// 开始第二个异步任务

    
string str3 = "T3_" + Guid.NewGuid().ToString();
    MyAysncClient
<string, string> client3 = new MyAysncClient<string, string>(ServiceUrl);
    client3.UserData
= "result3";
    client3.OnCallCompleted
+= new MyAysncClient<string, string>.CallCompletedEventHandler(client2_OnCallCompleted);
    client3.CallAysnc(str3, str3);        
// 开始第三个异步任务
}

void client2_OnCallCompleted(
object sender, MyAysncClient<string, string>.CallCompletedEventArgs e)
{
    
// 递减内部的异步任务累加器。有点类似AspNetSynchronizationContext的设计。
    AsyncManager.OutstandingOperations.Decrement();

    MyAysncClient
<string, string> client = (MyAysncClient<string, string>)sender;
    
string key = client.UserData.ToString();

    
if( e.Error == null )
        AsyncManager.Parameters[key]
= string.Format("{0} => {1}", e.UserState, e.Result);
    
else
        AsyncManager.Parameters[key]
= string.Format("{0} => Error: {1}", e.UserState, e.Error.Message);
}

public ActionResult Test2Completed(string result1, string result2, string result3)
{
    ViewData[
"result1"] = result1;
    ViewData[
"result2"] = result2;
    ViewData[
"result3"] = result3;
    
return View();
}

   我来解释一下上面的代码是如何以异步方式工作的。首先,我们要把Controller的基类修改为AsyncController,代码如下:

public class HomeController : AsyncController

   假如我有一个同步的Action方法:Test1,它看起来应该是这样的:

public ActionResult Test1()
{
    
return View();
}

   首先,我需要把它的返回值改成void, 并把方法名称修改为Test1Async 。

  然后,在开始异步调用前,调用AsyncManager.OutstandingOperations.Increment();

  在异步完成时:

  1. 要调用AsyncManager.OutstandingOperations.Decrement();

  2. 将结果写入到AsyncManager.Parameters[]这个集合中。注意key的名字后面要用到。

  到这里,异步开发的任务算是做了一大半了。你可能会想我在哪里返回ActionResult呢?

  再来创建一个Test1Completed方法,签名应该是这个样子的:

  public ActionResult Test1Completed(string result)

  注意:方法中的参数名要和前面说过的写AsyncManager.Parameters[]的key名一致,包括数量。

  再后面的事情,我想您懂的,我就不多说了。

  再来说说我对【ASP.NET MVC的异步方式】这个设计的感受吧。

  简单说来就是:不够完美。

  要知道在这个例子中,我可是采用的基于事件的异步模式啊,在异步页中,哪有这些额外的调用?

  对于这个设计,我至少有2点不满意:

  1. AsyncManager.OutstandingOperations.Increment(); Decrement();由使用者来控制,容易出错。

  2. AsyncManager.Parameters[]这个bag设计方式也不爽,难道仅仅是为了简单?因为我可以在完成事件时,根据条件继续后面的异步任务,最终结果可能并不确定,因此后面的XXXXCompleted方法的签名就是个问题了。

  为什么在ASP.NET MVC中,这个示例需要调用Increment(); Decrement(),而在异步页中不需要呢?

  恐怕有些人会对此有好奇,我就告诉大家吧:这与AspNetSynchronizationContext有关。

  AspNetSynchronizationContext,真是个【成也萧何,败成萧何】的东西,在异步页为什么不需要我们调用类似Increment(); Decrement()的语句是因为,它内部也有个这样的累加器,不过,当时在设计基于事件的异步模式时,在ASP.NET运行环境中,SynchronizationContext就是使用了AspNetSynchronizationContext这个具体实现类,但它的绝大部分成员却是internal类型的。如果可以使用它,可以用一种简便地方式设置一个统一的回调委托:

if( this._syncContext.PendingOperationsCount > 0 ) {
    this._syncContext.SetLastCompletionWorkItem(this._callHandlersThreadpoolCallback);
}

 

  就这么一句话,可以不用操心使用者到底开始了多少个异步任务,都可以在所有的异步结束后,回调指定的委托。只是可惜的是,这二个成员都是internal的!

  如果当初微软设计AspNetSynchronizationContext时,不开放SetLastCompletionWorkItem这个方法,是担心使用者乱调用导致ASP.NET运行错误的话,现在ASP.NET MVC的这种设计显然更容易出错。当然了,ASP.NET MVC出来的时候,这一切早就出现了,因此它也无法享受AspNetSynchronizationContext的便利性。不过,最让我想不通的是:直到ASP.NET 4.0,这一切还是原样。难道是因为ASP.NET MVC独立在升级,连InternalsVisibleTo的机会也不给它吗?

  就算我们不用基于事件的异步模式,异步页还有二种实现方法呢(都不需要累加器),可是ASP.NET MVC却没有实现类似的功能。所以,这样就显得很不完善。我们也只能期待未来的版本能改进这些问题了。

  MSDN参考文章:在 ASP.NET MVC 中使用异步控制器

 

0
相关文章