详细解读ASP.NET的异步

作者:   出处:互联网   2015-07-07 22:03:20   阅读:4

在前文中,介绍了.NET下的多种异步的形式,在WEB程序中,天生就是多线程的,因此使用异步应该更为谨慎。本文将着重展开ASP.NET中的异步。


 

【注意】本文中提到的异步指的是服务器端异步,而非客户端异步(Ajax)。


 

对于HTTP的请求响应模型,服务器无法主动通知或回调客户端,当客户端发起一个请求后,必须保持连接等待服务器的返回结果,才能继续处理,因此,对于客户端来说,请求与响应是无法异步进行,也就是说无论服务器如何处理请求,对于客户端来说没有任何差别。


 


 

那么ASP.NET异步指的又是什么,解决了什么问题呢?


 

在解释ASP.NET异步前,先来考察下ASP.NET线程模型。


 

ASP.NET线程模型


 

们知道,一个WEB服务可以同时服务器多个用户,们可以想象一下,WEB程序应该运行于多线程环境中,对于运行WEB程序的线程,们可以称之为WEB线程,那么,先来看看WEB线程长什么样子吧。


 

们可以用一个HttpHandler输出一些内容。


 

public class Handler : IHttpHandler  {  public void ProcessRequest(HttpContext context)  {  context.Response.ContentType =  text/plain ;  var thread = Thread.CurrentThread;  context.Response.Write(  string.Format( Name:{0}\r\nManagedThreadId:{1}\r\nIsBackground:{2}\r\nIsThreadPoolThread:{3} ,   thread.Name,  thread.ManagedThreadId,  thread.IsBackground,  thread.IsThreadPoolThread)  );  }  public bool IsReusable  {  get {return true;}  }  } 


 

你可以看到类似于这样的结果:


 

Name:


 

ManagedThreadId:57


 

IsBackground:True


 

IsThreadPoolThread:True


 

这里可以看到,WEB线程是一个没有名称的线程池中的线程,如果刷新这个页面,还有机会看到 ManagedThreadId 在不断变化,并且可能重复出现。说明WEB程序有机会运行于线程池中的不同线程。


 

为了模拟多用户并发访问的情况,们需要对这个处理程序添加人为的延时,并输出线程相关信息与开始结束时间,再通过客户端程序同时发起多个请求,查看返回的内容,分析请求的处理情况。


 

public void ProcessRequest(HttpContext context)  {  DateTime begin = DateTime.Now;  int t1, t2, t3;  ThreadPool.GetAvailableThreads(out t1, out t3);  ThreadPool.GetMaxThreads(out t2, out t3);  Thread.Sleep(TimeSpan.FromSeconds(10));  DateTime end = DateTime.Now;  context.Response.ContentType =  text/plain ;  var thread = Thread.CurrentThread;  context.Response.Write(  string.Format( TId:{0}\tApp:{1}\tBegin:{2:mm:ss,ffff}\tEnd:{3:mm:ss,ffff}\tTPool:{4} ,   thread.ManagedThreadId,  context.ApplicationInstance.GetHashCode(),  begin,  end,  t2 - t1  )  );  } 


 

们用一个命令行程序来发起请求,并显示结果。


 

static void Main()  {  var url = new Uri( http://localhost:8012/Handler.ashx );  var num = 50;  for (int i = 0; i   num; i++)  {  var request = WebRequest.Create(url);  request.GetResponseAsync().ContinueWith(t =  {  var stream = t.Result.GetResponseStream();  using (TextReader tr = new StreamReader(stream))  {  Console.WriteLine(tr.ReadToEnd());  }  });  }  Console.ReadLine();  } 


 

这里,们同时发起了50个请求,然后观察响应的情况。


 

【注意】后面的结果会因为操作系统、IIS版本、管道模式、.NET版本、配置项 的不同而不同,以下结果为在Windows Server 2008 R2 + IIS7.5 + .NET 4.5 beta(.NET 4 runtime) + 默认配置 中测试的结果,在没有特别说明的情况下,均为重启IIS后第一次运行的情况。

这个程序在电脑运行结果是这样的: 


 

TId:6 App:35898671 Begin:55:30,3176 End:55:40,3182 TPool:2  TId:5 App:22288629 Begin:55:30,3176 End:55:40,3212 TPool:2  TId:7 App:12549444 Begin:55:31,0426 End:55:41,0432 TPool:3  TId:8 App:22008501 Begin:55:31,5747 End:55:41,5752 TPool:4  TId:9 App:37121646 Begin:55:32,1067 End:55:42,1073 TPool:5  TId:10 App:33156464 Begin:55:32,6387 End:55:42,6393 TPool:6  TId:11 App:7995840 Begin:55:33,1707 End:55:43,1713 TPool:7  TId:12 App:36610825 Begin:55:33,7028 End:55:43,7033 TPool:8  TId:13 App:20554616 Begin:55:34,2048 End:55:44,2054 TPool:9  TId:14 App:15510466 Begin:55:35,2069 End:55:45,2074 TPool:10  TId:15 App:23324256 Begin:55:36,2049 End:55:46,2055 TPool:11  TId:16 App:34250480 Begin:55:37,2050 End:55:47,2055 TPool:12  TId:17 App:58408916 Begin:55:38,2050 End:55:48,2056 TPool:13  TId:18 App:2348279 Begin:55:39,2051 End:55:49,2057 TPool:14  TId:19 App:61669314 Begin:55:40,2051 End:55:50,2057 TPool:15  TId:6 App:35898671 Begin:55:40,3212 End:55:50,3217 TPool:15  TId:5 App:22288629 Begin:55:40,3232 End:55:50,3237 TPool:15  TId:7 App:12549444 Begin:55:41,0432 End:55:51,0438 TPool:15  TId:8 App:22008501 Begin:55:41,5752 End:55:51,5758 TPool:15  TId:9 App:37121646 Begin:55:42,1073 End:55:52,1078 TPool:15  TId:10 App:33156464 Begin:55:42,6393 End:55:52,6399 TPool:15  TId:11 App:7995840 Begin:55:43,1713 End:55:53,1719 TPool:15  TId:12 App:36610825 Begin:55:43,7043 End:55:53,7049 TPool:15  TId:13 App:20554616 Begin:55:44,2054 End:55:54,2059 TPool:15  TId:20 App:36865354 Begin:55:45,2074 End:55:55,2080 TPool:16  TId:14 App:15510466 Begin:55:45,2084 End:55:55,2090 TPool:16  TId:21 App:3196068 Begin:55:46,2055 End:55:56,2061 TPool:17  TId:15 App:23324256 Begin:55:46,2065 End:55:56,2071 TPool:17  TId:22 App:4186222 Begin:55:47,2055 End:55:57,2061 TPool:18  TId:16 App:34250480 Begin:55:47,2065 End:55:57,2071 TPool:18  TId:23 App:764807 Begin:55:48,2046 End:55:58,2052 TPool:19  TId:17 App:58408916 Begin:55:48,2056 End:55:58,2062 TPool:19  TId:24 App:10479095 Begin:55:49,2047 End:55:59,2052 TPool:20  TId:18 App:2348279 Begin:55:49,2057 End:55:59,2062 TPool:20  TId:25 App:4684807 Begin:55:50,2047 End:56:00,2053 TPool:21  TId:19 App:61669314 Begin:55:50,2057 End:56:00,2063 TPool:21  TId:6 App:35898671 Begin:55:50,3227 End:56:00,3233 TPool:21  TId:5 App:22288629 Begin:55:50,3237 End:56:00,3243 TPool:21  TId:7 App:12549444 Begin:55:51,0438 End:56:01,0443 TPool:21  TId:8 App:22008501 Begin:55:51,5758 End:56:01,5764 TPool:21  TId:9 App:37121646 Begin:55:52,1078 End:56:02,1084 TPool:21  TId:10 App:33156464 Begin:55:52,6399 End:56:02,6404 TPool:21  TId:11 App:7995840 Begin:55:53,1719 End:56:03,1725 TPool:21  TId:26 App:41662089 Begin:55:53,7049 End:56:03,7055 TPool:22  TId:12 App:36610825 Begin:55:53,7059 End:56:03,7065 TPool:22  TId:13 App:20554616 Begin:55:54,2069 End:56:04,2075 TPool:22  TId:27 App:46338128 Begin:55:55,2070 End:56:05,2076 TPool:23  TId:14 App:15510466 Begin:55:55,2090 End:56:05,2096 TPool:23  TId:20 App:36865354 Begin:55:55,2090 End:56:05,2096 TPool:23  TId:28 App:28975576 Begin:55:56,2051 End:56:06,2056 TPool:24 


 

从这个结果大概可以看出,开始两个请求几乎同时开始处理,因为线程池最小线程数为2(可配置),紧接着后面的请求会每隔半秒钟开始一个,因为如果池中的线程都忙,会等待半秒(.NET版本不同而不同),如果还是没有线程释放则开启新的线程,直到达到最大线程数(可配置)。未能在线程池中处理的请求将被放入请求队列,当一个线程释放后,下一个请求紧接着开始在该线程处理。


 

最终50个请求共产生24个线程,总用时约35.9秒。


 

光看数据不够形象,用简单的代码把数据转换成图形吧,下面是100个请求的处理过程。


 


 

们可以看到,当WEB线程长时间被占用时,请求会由于线程池而阻塞,同时产生大量的线程,最终响应时间变长。


 

作为对比,们列出处理时间10毫秒的数据。


 

TId:6 App:44665200 Begin:41:07,9932 End:41:08,0032 TPool:2  TId:5 App:37489757 Begin:41:07,9932 End:41:08,0032 TPool:2  TId:5 App:44665200 Begin:41:08,0042 End:41:08,0142 TPool:2  TId:6 App:37489757 Begin:41:08,0052 End:41:08,0152 TPool:2  TId:5 App:44665200 Begin:41:08,0142 End:41:08,0242 TPool:2  TId:6 App:37489757 Begin:41:08,0152 End:41:08,0252 TPool:2  TId:5 App:44665200 Begin:41:08,0242 End:41:08,0342 TPool:2  TId:6 App:37489757 Begin:41:08,0252 End:41:08,0352 TPool:2  TId:5 App:44665200 Begin:41:08,0342 End:41:08,0442 TPool:2  TId:6 App:37489757 Begin:41:08,0352 End:41:08,0452 TPool:2  TId:5 App:44665200 Begin:41:08,0442 End:41:08,0542 TPool:2  TId:6 App:37489757 Begin:41:08,0452 End:41:08,0552 TPool:2  TId:5 App:44665200 Begin:41:08,0542 End:41:08,0642 TPool:2  TId:6 App:37489757 Begin:41:08,0552 End:41:08,0652 TPool:2  TId:5 App:44665200 Begin:41:08,0642 End:41:08,0742 TPool:2  TId:6 App:37489757 Begin:41:08,0652 End:41:08,0752 TPool:2  TId:5 App:44665200 Begin:41:08,0742 End:41:08,0842 TPool:2  TId:6 App:37489757 Begin:41:08,0752 End:41:08,0852 TPool:2  TId:5 App:44665200 Begin:41:08,0842 End:41:08,0942 TPool:2  TId:6 App:37489757 Begin:41:08,0852 End:41:08,0952 TPool:2  TId:5 App:44665200 Begin:41:08,0942 End:41:08,1042 TPool:2  TId:6 App:37489757 Begin:41:08,0952 End:41:08,1052 TPool:2  TId:5 App:44665200 Begin:41:08,1042 End:41:08,1142 TPool:2  TId:6 App:37489757 Begin:41:08,1052 End:41:08,1152 TPool:2  TId:5 App:44665200 Begin:41:08,1142 End:41:08,1242 TPool:2  TId:6 App:37489757 Begin:41:08,1152 End:41:08,1252 TPool:2  TId:5 App:44665200 Begin:41:08,1242 End:41:08,1342 TPool:2  TId:6 App:37489757 Begin:41:08,1252 End:41:08,1352 TPool:2  TId:5 App:44665200 Begin:41:08,1342 End:41:08,1442 TPool:2  TId:6 App:37489757 Begin:41:08,1352 End:41:08,1452 TPool:2  TId:5 App:44665200 Begin:41:08,1442 End:41:08,1542 TPool:2  TId:6 App:37489757 Begin:41:08,1452 End:41:08,1552 TPool:2  TId:5 App:44665200 Begin:41:08,1542 End:41:08,1642 TPool:2  TId:6 App:37489757 Begin:41:08,1552 End:41:08,1652 TPool:2  TId:5 App:44665200 Begin:41:08,1642 End:41:08,1742 TPool:2  TId:6 App:37489757 Begin:41:08,1652 End:41:08,1752 TPool:2  TId:5 App:44665200 Begin:41:08,1742 End:41:08,1842 TPool:3  TId:7 App:12547953 Begin:41:08,1752 End:41:08,1852 TPool:3  TId:6 App:37489757 Begin:41:08,1762 End:41:08,1862 TPool:3  TId:5 App:44665200 Begin:41:08,1842 End:41:08,1942 TPool:3  TId:7 App:12547953 Begin:41:08,1852 End:41:08,1952 TPool:3  TId:6 App:37489757 Begin:41:08,1862 End:41:08,1962 TPool:3  TId:5 App:44665200 Begin:41:08,1942 End:41:08,2042 TPool:3  TId:7 App:12547953 Begin:41:08,1952 End:41:08,2092 TPool:3  TId:6 App:37489757 Begin:41:08,1962 End:41:08,2102 TPool:3  TId:5 App:44665200 Begin:41:08,2052 End:41:08,2152 TPool:3  TId:7 App:12547953 Begin:41:08,2092 End:41:08,2192 TPool:3  TId:6 App:37489757 Begin:41:08,2102 End:41:08,2202 TPool:3  TId:5 App:44665200 Begin:41:08,2152 End:41:08,2252 TPool:3  TId:7 App:12547953 Begin:41:08,2192 End:41:08,2292 TPool:3 


 

共产生线程3个,总用时0.236秒。


 

根据以上的数据,们可以得出结论,要提高系统响应时间与并发处理数,应尽可能减少WEB线程的等待。


 

【略】请各位自行查验当一次并发全部处理完毕后再次测试的处理情况。


 

【略】请各位自行查验当处理程序中使用线程池处理等待任务的处理情况。


 

如何减少WEB线程的等待呢,那就应该尽早的结果ProcessRequest方法,前一篇中讲到,对于一些需要等待完成的任务,可以使用异步方法来做,于是们可以在ProcessRequest中调用异步方法,但问题是当ProcessRequest结束后,请求处理也即将结束,一但请求结束,将没有办法在这一次请求中返回结果给客户端,但是此时,异步任务还没有完成,当异步任务完成时,也许再也没有办法将结果传给客户端了。(难道用轮询?囧)


 

们需要的方案是,处理请求时可以暂停处理(不是暂停线程),并保持客户端连接,在需要时,向客户端输出结果,并结束请求。


 


 

在这个模型中,可以看到,对于WebServerRuntime来说,们的请求处理程序就是一个异步方法,而对于客户端来说,却并不知道后面的处理情况。无论在WebServerRuntime或是们的处理程序,都没有直接占用线程,一切由何时SetComplete决定。同时可以看到,这种模式需要WebServerRuntime的紧密配合,提供调用异步方法的接口。在ASP.NET中,这个接口就是IHttpAsyncHandler。


 

异步ASP.NET处理程序


 

首先,们来实现第一个异步处理程序,在适当的时候触发结束,在开始和结束时输出一些信息。 


 

public class Handler : IHttpHandler, IHttpAsyncHandler  {  public void ProcessRequest(HttpContext context)  {  //异步处理器不执行该方法  }  public bool IsReusable  {  //设置允许重用对象  get { return false; }  }    //请求开始时由ASP.NET调用此方法  public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)  {  context.Response.ContentType =  text/xml ;  context.Response.Write( App: );  context.Response.Write(context.ApplicationInstance.GetHashCode());  context.Response.Write( \tBegin: );  context.Response.Write(DateTime.Now.ToString( mm:ss,ffff ));  //输出当前线程  context.Response.Write( \tThreadId: );  context.Response.Write(Thread.CurrentThread.ManagedThreadId);  //构建异步结果并返回  var result = new WebAsyncResult(cb, context);  //用一个定时器来模拟异步触发完成  Timer timer = null;  timer = new Timer(o =  {  result.SetComplete();  timer.Dispose();  }, null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5));  return result;  }  //异步结束时,由ASP.NET调用此方法  public void EndProcessRequest(IAsyncResult result)  {  WebAsyncResult webresult = (WebAsyncResult)result;  webresult.Context.Response.Write( \tEnd: );  webresult.Context.Response.Write(DateTime.Now.ToString( mm:ss,ffff ));  //输出当前线程  webresult.Context.Response.Write( \tThreadId: );  webresult.Context.Response.Write(Thread.CurrentThread.ManagedThreadId);  }  //WEB异步方法结果  class WebAsyncResult : IAsyncResult  {  private AsyncCallback _callback;  public WebAsyncResult(AsyncCallback cb, HttpContext context)  {  Context = context;  _callback = cb;  }  //当异步完成时调用该方法  public void SetComplete()  {  IsCompleted = true;  if (_callback != null)  {  _callback(this);  }  }  public HttpContext Context  {  get;  private set;  }  public object AsyncState  {  get { return null; }  }  //由于ASP.NET不会等待WEB异步方法,所以不使用此对象  public WaitHandle AsyncWaitHandle  {  get { throw new NotImplementedException(); }  }  public bool CompletedSynchronously  {  get { return false; }  }  public bool IsCompleted  {  get;  private set;  }  }  } 


 

在这里,们实现了一个简单的AsyncResult,由于ASP.NET通过回调方法获取异步完成,不会等待异步,所以不需要WaitHandle。在开始请求时,建立一个AsyncResult后直接返回,当异步完成时,调用AsyncResult的SetComplete方法,调用回调方法,再由 ASP.NET调用异步结束。此时整个请求即完成。


 

当们访问这个地址,可以得到类似于下面的结果:


 

App:11240144 Begin:37:24,2676 ThreadId:6 End:37:29,2619 ThreadId:6


 

可以看到开始和结束在同一个线程中运行。 


 


 

当有多个并发请求时,线程池将忙碌起来,开始与结束处理也奖有机会运行于不同的线程上。50个请求并发时的处理数据:


 

App:52307948 Begin:39:47,8128 ThreadId:6 End:39:52,8231 ThreadId:5  App:58766839 Begin:39:47,8358 ThreadId:5 End:39:52,8321 ThreadId:7  App:23825510 Begin:39:47,8348 ThreadId:5 End:39:52,8321 ThreadId:7  App:30480920 Begin:39:47,8348 ThreadId:5 End:39:52,8321 ThreadId:7  App:62301924 Begin:39:47,8348 ThreadId:6 End:39:52,8321 ThreadId:6  App:28062782 Begin:39:47,8338 ThreadId:5 End:39:52,8321 ThreadId:6  App:41488021 Begin:39:47,8338 ThreadId:6 End:39:52,8321 ThreadId:7  App:15315213 Begin:39:47,8338 ThreadId:6 End:39:52,8321 ThreadId:6  App:17228638 Begin:39:47,8328 ThreadId:5 End:39:52,8321 ThreadId:7  App:51438283 Begin:39:47,8328 ThreadId:6 End:39:52,8321 ThreadId:6  App:32901400 Begin:39:47,8328 ThreadId:5 End:39:52,8321 ThreadId:7  App:61925337 Begin:39:47,8358 ThreadId:6 End:39:52,8321 ThreadId:6  App:24914721 Begin:39:47,8318 ThreadId:6 End:39:52,8321 ThreadId:6  App:26314214 Begin:39:47,8318 ThreadId:6 End:39:52,8321 ThreadId:6  App:51004322 Begin:39:47,8358 ThreadId:6 End:39:52,8321 ThreadId:6  App:51484875 Begin:39:47,8308 ThreadId:5 End:39:52,8321 ThreadId:7  App:19420176 Begin:39:47,8308 ThreadId:6 End:39:52,8321 ThreadId:6  App:16868352 Begin:39:47,8298 ThreadId:6 End:39:52,8321 ThreadId:7  App:61115195 Begin:39:47,8298 ThreadId:5 End:39:52,8321 ThreadId:6  App:63062333 Begin:39:47,8288 ThreadId:6 End:39:52,8321 ThreadId:6  App:53447344 Begin:39:47,8298 ThreadId:5 End:39:52,8321 ThreadId:7  App:31665793 Begin:39:47,8288 ThreadId:5 End:39:52,8321 ThreadId:7  App:2174563 Begin:39:47,8288 ThreadId:6 End:39:52,8321 ThreadId:6  App:12053474 Begin:39:47,8318 ThreadId:5 End:39:52,8321 ThreadId:7  App:41728762 Begin:39:47,8278 ThreadId:6 End:39:52,8321 ThreadId:6  App:6385742 Begin:39:47,8278 ThreadId:5 End:39:52,8321 ThreadId:7  App:13009416 Begin:39:47,8268 ThreadId:6 End:39:52,8321 ThreadId:6  App:43205102 Begin:39:47,8268 ThreadId:5 End:39:52,8321 ThreadId:7  App:14333193 Begin:39:47,8268 ThreadId:6 End:39:52,8321 ThreadId:6  App:2808346 Begin:39:47,8258 ThreadId:6 End:39:52,8321 ThreadId:6  App:37489757 Begin:39:47,8128 ThreadId:5 End:39:52,8231 ThreadId:6  App:34106743 Begin:39:47,8258 ThreadId:5 End:39:52,8321 ThreadId:7  App:30180123 Begin:39:47,8248 ThreadId:6 End:39:52,8321 ThreadId:6  App:44313942 Begin:39:47,8248 ThreadId:5 End:39:52,8321 ThreadId:7  App:12611187 Begin:39:47,8248 ThreadId:6 End:39:52,8321 ThreadId:6  App:7141266 Begin:39:47,8238 ThreadId:5 End:39:52,8321 ThreadId:7  App:25425822 Begin:39:47,8278 ThreadId:5 End:39:52,8321 ThreadId:7  App:51288387 Begin:39:47,8238 ThreadId:5 End:39:52,8321 ThreadId:7  App:66166301 Begin:39:47,8228 ThreadId:6 End:39:52,8321 ThreadId:6  App:34678979 Begin:39:47,8228 ThreadId:6 End:39:52,8321 ThreadId:7  App:10104599 Begin:39:47,8218 ThreadId:5 End:39:52,8321 ThreadId:6  App:47362231 Begin:39:47,8258 ThreadId:5 End:39:52,8321 ThreadId:7  App:40535505 Begin:39:47,8218 ThreadId:6 End:39:52,8321 ThreadId:7  App:20726372 Begin:39:47,8368 ThreadId:5 End:39:52,8321 ThreadId:5  App:2730334 Begin:39:47,8368 ThreadId:6 End:39:52,8321 ThreadId:6  App:59884855 Begin:39:47,8368 ThreadId:5 End:39:52,8321 ThreadId:7  App:39774547 Begin:39:47,8238 ThreadId:6 End:39:52,8321 ThreadId:6  App:12070837 Begin:39:47,8378 ThreadId:6 End:39:52,8491 ThreadId:7  App:64828693 Begin:39:47,8218 ThreadId:5 End:39:52,8331 ThreadId:6  App:14509978 Begin:39:47,9308 ThreadId:6 End:39:52,9281 ThreadId:5 


 

可以看到,从始至终只由3个线程处理所有的请求,总共时间约5.12秒。


 

为简化分析,们用下面的图来示意异步处理程序的并发处理过程。


 


 

这样,们就可以通过异步的方式,将WEB线程撤底释放出来。由WEB线程进行请求的接收与结束处理,耗时的操作与等待都进行异步处理。这样少量的WEB线程就可以承受大量的并发请求,WEB线程将不再成为系统的瓶颈。


 

在大并发的异步模式下,和前面的数据相比较,可以看到HttpApplication的对象数量随并发处理数提高而提高,随之带来的一系列数据结构,如 HttpHandler缓存,是需要考虑的内存开销。同时,在异步模式下,请求的完成需要编程的方式来控制,在触发完成前,客户端连接、 HttpContext对象都保持活动状态,客户端也一直保持等待,直到超时。因此,异步模式下需要更细致的资源操作。


 

们来看ASP.NET异步 的典型应用场景。


 

场景一:处理过程中有需要等待的任务,并且可以使用异步完成的。


 

//同步方法  public void ProcessRequest(HttpContext context)  {  FileStream fs = new FileStream( , FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 4096, FileOptions.Asynchronous);  fs.CopyTo(context.Response.OutputStream);  }    //异步方法开始  public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)  {  FileStream fs = new FileStream( D:\\a.txt , FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 4096, FileOptions.Asynchronous);  var task = fs.CopyToAsync(context.Response.OutputStream);  task.GetAwaiter().OnCompleted(() =  cb(task));  return task;  }  //异步方法结束  public void EndProcessRequest(IAsyncResult result)  {  } 


 

这个处理程序读取服务器的文件并输出到客户端。


 

//同步方法  public void ProcessRequest(HttpContext context)  {  var url = context.Request.QueryString[ url ];  var request = (HttpWebRequest)WebRequest.Create(url);  var response = request.GetResponse();  var stream = response.GetResponseStream();  stream.CopyTo(context.Response.OutputStream);  }    //异步方法开始  public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)  {  //构建异步结果并返回  var result = new WebAsyncResult(cb, context);  var url = context.Request.QueryString[ url ];  var request = (HttpWebRequest)WebRequest.Create(url);  var responseTask = request.GetResponseAsync();  responseTask.GetAwaiter().OnCompleted(() =  {  var stream = responseTask.Result.GetResponseStream();  stream.CopyToAsync(context.Response.OutputStream).GetAwaiter().OnCompleted(() =  {  result.SetComplete();  });  });  return result;  }  //异步方法结束  public void EndProcessRequest(IAsyncResult result)  {  } 


 

这是一个简单的代理,服务器获取WEB资源后写回。


 

在这类程序中,们提供的异步处理程序调用了IOCP异步方法,使得大量节省了WEB线程的占用,相比同步处理程序来说,并发量会得到相当大的提升。


 

【注意】前面提到,由于WEB线程属于线程池线程,因此,如果在线程池中加入任务,将同样会影响并发处理数。而在异步处理程序中,由线程池来完成异步将得不到任何本质上的提升,因此在异步处理程序中禁止操作线程池(ThreadPool.QueueUserWorkItem、 delegate.BeginInvoke,Task.Run等)。如果确定需要使用多线程来处理大量的计算,需要自己开启线程或实现自己的线程池。


 

public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)  {  return new Action(() =  {  Thread.Sleep(1000);  context.Response.Write( OK );  }).BeginInvoke(cb, extraData);  } 


 

上面的代码将无法达到异步的效果。


 


 

虽然等待工作交由另一线程去操作,但是该线程与WEB线程性质相同,同样会导致其他请求阻塞。


 

【思考】如果们的程序中的确需要有大量的计算,那么可以考虑将这些计算提取到独立的应用服务器中,然后通过网络IOCP异步调用,达到WEB服务器的高吞吐量与系统的平行扩展性。


 

典型应用场景二:长连接消息推送。


 

一般来说,在WEB中获取服务器消息,采用轮询的方式,这种方式不可避免会有延时,当们需要即时消息的推送时(如WEBIM),需要用到长连接。


 

长连接方式,由客户端发起请求,服务器端接收后暂停处理并保持连接,当需要发送消息给客户端时,输出内容并结束处理,客户端得到消息或者超时后,再次发起连接。如此达到在HTTP协议上服务器消息即时推送到客户端的目的。


 


 

在这种情况下,们希望服务器尽可能长时间保持连接,如果采用同步处理程序,则连接数受到服务器线程数的限制,而异步处理程序则可以很好的解决这个问题。异步处理程序开始时,收集相关信息,并放入集合后返回异步结果。当需要向这个客户端发送消息时,从客户端集合中找到需要发送的目标,发送完成即可。


 

首先,们需要对客户端进行标识,这个标识往往采用sessionid来做,本例中简单起见,通过客户端传递参数获取。


 

public class WebAsyncResult : IAsyncResult  {  private AsyncCallback _callback;  public WebAsyncResult(AsyncCallback cb, HttpContext context, string clientID)  {  Context = context;  ClientID = clientID;  _callback = cb;  }  //当异步完成时调用该方法  public void SetComplete()  {  IsCompleted = true;  if (_callback != null)  {  _callback(this);  }  } 


 

们需要一个集合来保存连接中的客户端,提供一个向这些客户端发送消息的方法。


 

public class WebAsyncResultCollection : List WebAsyncResult , ICollection WebAsyncResult  {  private static WebAsyncResultCollection _instance = new WebAsyncResultCollection();  public static WebAsyncResultCollection Instance  {  get { return WebAsyncResultCollection._instance; }  }  public bool SendMessage(string clientID, string message)  {  var result = this.FirstOrDefault(r =  r.ClientID == clientID);  if (result != null)  {  Remove(result);  bool sendsuccess = false;  if (result.Context.Response.IsClientConnected)  {  sendsuccess = true;  result.Context.Response.Write(message);  }  result.SetComplete();  return sendsuccess;  }  return false;  }  } 


 

对于异步处理程序的开始方法,们收集信息并放入集合。


 

public IAsyncResult BeginProcessRequest(HttpContext context, AsyncCallback cb, object extraData)  {  var clientID = context.Request.QueryString[ id ];  WebAsyncResultCollection.Instance.SendMessage(clientID,  ClearClientID );  WebAsyncResult result = new WebAsyncResult(cb, context, clientID);  WebAsyncResultCollection.Instance.Add(result);  return result;  } 


 

【不完善】由于客户端收到一次消息后结束请求,由客户端再次发起请求,中间会有部分时间间隙,在这间隙中向该客户端发送的消息将丢失,解决方案是维护另一个用户是否在线的表,如果用户不在线,则处理离线消息,如果在线,并且正在连接中,则按上述处理,如果不在连接中,则缓存在服务器,当客户端再次连接时,首先检查缓存的消息,如果有未接消息,则获取消息并立即返回。


 

发送消息的处理程序。


 

public class SendMessage : IHttpHandler  {  public void ProcessRequest(HttpContext context)  {  var clientID = context.Request.QueryString[ clientID ];  var message = context.Request.QueryString[ message ];  WebAsyncResultCollection.Instance.SendMessage(clientID, message);  }  public bool IsReusable  {  get {  return true;  }  }  } 


 

可以在任何需要的位置向客户端发送消息。


 

【不完善】们需要定时刷新客户端集合,对于长时间未处理的客户端进行超时结束处理。


 

通过异步处理程序构建的长连接消息推送机制,单台服务器可以轻松支持上万个并发连接。


 

异步Action


 

在ASP.NET MVC 4中,添加了对异步Action的支持。   


 


 

在ASP.NET MVC4中,整个处理过程都是异步的。


 

在图中可以看到,最右边的ActionDescriptor将决定如何调用们的Action方法,而如何调用是由具体的Action方法形式决定,ASP.NET MVC会根据不同的方法形式创建不同的ActionDescriptor实例,从而调用不同的处理过程。对于传统的方法,则使用 ReflectedActionDescriptor,他实现Execute方法,调用们的Action,并在 AsyncControllerActionInvoker包装成同步调用。而异步调用在ASP.NET MVC 4  中有两种模式。


 

异步Action模式一:AsyncController/XXXAsync/XXXCompleted


 

们可以使一个Controller继承自AsyncController,按照约定同时提供两个方法,分别命名为 XXXAsync/XXXCompleted,ASP.NET MVC则会将他们包装成ReflectedAsyncActionDescriptor。 


 

public class DefaultController : AsyncController  {  public void DoAsync()  {  //注册一次异步  AsyncManager.OutstandingOperations.Increment();  Timer timer = null;  timer = new Timer(o =  {  //一次异步完成  AsyncManager.OutstandingOperations.Decrement();  timer.Dispose();  },null, 5000, 5000);  }  public ActionResult DoCompleted()  {  return Content( OK );  }  } 


 

由于没有IAsyncResult,们需要通过AsyncManager来告诉ASP.NET MVC何时完成异步,们可以在方法内部在启用异步时调用 AsyncManager.OutstandingOperations.Increment()告诉ASP.NET MVC开始了一次异步,完成异步时调用AsyncManager.OutstandingOperations.Decrement()告诉 ASP.NET MVC完成了一次异步,当所有异步完成,AsyncManager会自动触发异步完成事件,调用回调方法,最终调用们的XXXComplete方法。们也可以用AsyncManager.Finish()也触发所有异步完成。当不使用任何AsyncManager时,则不启用异步。


 


 

可以看到整个异步过程由ASP.NET完成,在适当的时候会调用们的方法。异步的开始、结束动作与及如何触发完成都在们的代码中体现。


 

异步Action模式二:Task Action


 

对于Action,如果返回的类型是 Task,ASP.NET MVC则会将他们包装成TaskAsyncActionDescriptor。 


 

public class DefaultController : Controller  {  public async Task FileResult  Download()  {  using (FileStream fs = new FileStream( D:\\a.txt , FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 4096, FileOptions.Asynchronous))  {  byte[] data = new byte[fs.Length];  await fs.ReadAsync(data, 0, data.Length);  return new FileContentResult(data,  application/octet-stream );  }  }  } 


 

只需要需提供一个返回类型为Task的方法即可,里们采用async/await语法构建一个异步方法,在方法内部调用其他的异步方法。


 


 

相比之前的模式,简单了一些,特别是们的Controller中,只有一个方法,异步的操作都交由Task完成。对于可以返回Task的方法来说(如通过async/await包装多个异步方法),就显得十分方便。

Copyright© 2006-2015 ComponentCN.com all rights reserved.重庆磐岩科技有限公司(控件中国网) 版权所有 渝ICP备12000264号 法律顾问:元炳律师事务所
客服软件
live chat