【转】详解 ASP.NET异步

在说到异步前,先来理一下几个容易混淆的概念,并行、多线程、异步。

    并行,一般指并行计算,是说同一时刻有多条指令同时被执行,这些指令可能执行于同一CPU的多核上,或者多个CPU上,或者多个物理主机甚至多个网络中。

    多线程,一般指同一进程中多个线程(包含其数据结构、上下文与代码片段)协作运行。在多核计算机中多个线程将有机会同时运行于多个核上,如果线程中进行的是计算,则行成并行计算。

    异步,与同步相对应,是指呼叫另一操作后,不等待其结果,继续执行之后的操作,若之后没有其他操作,当前线程将进入睡眠状态,而CPU时间将有机会切至其他线程。在异步操作完成后通过回调函数的方式获取通知与结果。异步的实现方式有多种,如多线程与完成端口。多线程将异步操作放入另一线程中运行,通过轮询或回调方法得到完成通知;完成端口,由操作系统接管异步操作的调度,通过硬件中断,在完成时触发回调方法,此方式不需要占用额外线程。

 

    本文讨论.NET下的异步,以及其进化过程中出现的多种异步模式。

 

    首先看一下两段需要花较长时间运行的代码在同步方式下的情形。  

复制代码
public class ProgramClass 
{
public static void Main()
{
using (var fs = new FileStream("Data.dat", FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, 4096))
{
fs.Write(new byte[100], 0, 100);
}
DoSomething();
Console.WriteLine("END");
}
static string DoSomething()
{
Thread.Sleep(2000);
return "Finished";
}
}
 

    同步方式运行时,所有操作会顺序执行,当某方法被阻塞时,线程即进入阻塞状态。该情形下,CPU时间无法得到充分利用,当前线程长时间处于阻塞状态,任务总时间长。

   

  

    开始异步化

 

    为提高CPU使用率,从而减少任务时间,采用多线程方式实现异步调用。

复制代码
public class ProgramClass {     public static void Main()     {         Thread writeThread = new Thread(new ThreadStart(WriteWapper));         Thread doSomethingThread = new Thread(new ParameterizedThreadStart(DoSomethingWapper));
ClosureClass closure = new ClosureClass(); writeThread.Start(); doSomethingThread.Start(closure);//闭包对象,用于变量穿越 writeThread.Join(); doSomethingThread.Join();
Console.WriteLine(closure.Result); }
//将方法包装成适于线程调用的签名 private static void WriteWapper() { using (var fs = new FileStream("Data.dat", FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, 4096)) { fs.Write(new byte[100], 0, 100); } }
//将方法包装成适于线程调用的签名 static void DoSomethingWapper(object state) { ClosureClass closure = state as ClosureClass; var result = DoSomething(); if (closure != null) { closure.Result = result; } }
static string DoSomething() { Thread.Sleep(2000); return "Finished"; }
//闭包辅助类,用于存储在方法间传递内部变量与参数 class ClosureClass { //存储方法返回值 public string Result { get; set; } } }
复制代码

    利用多线程将耗时操作放入其他线程中进行处理,主线程继续做自己的事(本例中,主线程进行等待其他线程完成)。从而减少任务处理时间。

    【注意】本例中,write与dosomething操作内部均有线程等待,在单核中依然可以通过操作系统的线程切换提高CPU使用率,但是如果操作是需要大量CPU计算,则在单核情况下并不一定能够提高CPU使用率,并且可能增加线程调试的开销,因此单核情况下此种方式不适合用于密集型运算。

    【提示】对于线程的入口方法,我们往往会对其进行包装,形成一致的方法签名、处理异常、拦截请求等。在本例中,由于被调用的方法有输入与输出,困此采用辅助对象进行传递,在C#2开始引入的闭包,采用类似的原理实现,从而减少大量的代码,并提高程序可读性。

复制代码
public class ProgramClass {     
public static void Main()
{
string result = null;
Thread writeThread = new Thread(new ThreadStart(WriteWapper));
Thread doSomethingThread = new Thread(new ThreadStart(() =>
{
result = DoSomething();//跨方法访问临时变量,形成闭包
}));
writeThread.Start();
doSomethingThread.Start();
writeThread.Join();
doSomethingThread.Join();
Console.WriteLine(result);
}
//将方法包装成适于线程调用的签名
private static void WriteWapper()
{
using (var fs = new FileStream("Data.dat", FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, 4096))
{
fs.Write(new byte[100], 0, 100);
}
}
static string DoSomething()
{
Thread.Sleep(2000);
return "Finished";
}
}
复制代码

    开启一个新线程将带来可观的开销,因此我们希望能够重用线程,在.NET中,可以采用线程池达到这一目的,同时简化线程的操作。

复制代码
public class ProgramClass 
{
public static void Main()
{
string result = null;
AutoResetEvent resetEvent = new AutoResetEvent(false);
ThreadPool.QueueUserWorkItem(new WaitCallback(state =>
{
result = DoSomething();
resetEvent.Set();
}));
resetEvent.WaitOne();
Console.WriteLine(result);
}
static string DoSomething()
{
Thread.Sleep(2000);
return "Finished";
}
}
复制代码

    由于线程池中,我们无法对线程进行更为细致的操作,为得到操作完成的通知,我们需要在包装方法中,在操作完成后加入适当的代码,本例中我们采用ResetEvent进行线程的同步。

    【注意】在ASP.NET中,所有的WEB线程均运行于线程池,因此线程池中的线程是非常宝贵的资源,耗尽线程池中的线程将可能引起所有的请求进入等待队列,从而无法提供服务,在ASP.NET中的线程池操作应该更为谨慎。

 

 

    完成端口与异步模型

 

    到这里为止,都是采用多线程的方式手动实现了异步,正如前面所说,多线程不适用于单核密集运算,在非密集运算下也会产生线程调度的开销,在需要大量线程的应用中会浪费宝贵资源。考察需要阻塞等待的场景,往往是与系统外部数据交换有关,如大量内存数据的复制、读写磁盘文件、访问网络等,这种情况下,在硬件完成操作前CPU无能为力,因此只能等待,更完美的方案是发出指令后不进入等待,当操作完毕后通过某种方式得到通知并执行相关代码,称为完成端口。

    完成端口编程复杂,并且需要操作系统支持,使用中需要先判断是否支持,再采用不同的方式去实现,并且实现的方法多样,在异步使用频繁的今天,为简化异步操作,往往会制订一种统一的异步模型,并且这类模型也在不断进化中。

    在介绍异步模型时,我们会用不同的方法先将一个普通方法异步调用,再调用类库中提供的异步方法,然后实现一个自己的异步方法,最后将多个异步方法按顺序调用包装成新的异步方法。

    在早期的.NET中,采用 BeginXXX/EndXXX 方式实现异步。

    对于普通的方法,可以采用委托的 BeginInvoke / EndInvoke 实现异步化。

  

复制代码
public class ProgramClass {     public static void Main()     {         string result = null;
var doSomgthingDelegate = new Func<string>(DoSomething); var asyncResult = doSomgthingDelegate.BeginInvoke(new AsyncCallback(aresult => { result = doSomgthingDelegate.EndInvoke(aresult); }), null);
asyncResult.AsyncWaitHandle.WaitOne();
Console.WriteLine(result); }
static string DoSomething() { Thread.Sleep(2000); return "Finished"; } }
复制代码

    委托的异步内部采用线程池实现。

    有些类库中的方法,实现了异步版本。

复制代码
public static void Main() {     using (var fs = new FileStream("Data.dat", FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, 4096         , FileOptions.Asynchronous))     {         fs.Write(new byte[100], 0, 100);         var asyncResult = fs.BeginWrite(new byte[100], 0, 100, new AsyncCallback(aresult => {             fs.EndWrite(aresult);//执行完毕后的回调方法        }), null);
asyncResult.AsyncWaitHandle.WaitOne(); } }
复制代码

    对于类库的方法的异步版本,内部会进行判断决定采用何种方式实现。

    【注意】对于FileStream,必须加上FileOptions.Asynchronous才会有机会使用完成端口。

    现在我们可以根据这个模型来实现自己的异步方法。

复制代码
public class ProgramClass {     public static void Main()     {         DoSomeThing();
var result = BeginDoSomeThing(1, new AsyncCallback(aresult => { ProgramClass.EndDoSomeThing(aresult); }), null); result.AsyncWaitHandle.WaitOne(); }
//同步版本 public static string DoSomeThing() { Thread.Sleep(2000); return "Finished"; }
//异步版本开始 public static IAsyncResult BeginDoSomeThing(int arg1, AsyncCallback callback, object state) { var asyncResult = new DoSomethingAsyncResult(callback, state);
Timer timer = null; timer = new Timer(new TimerCallback(s => { timer.Dispose(); asyncResult.SetComplete("Finished"); }), state, TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(2));
return asyncResult; }
//异步版本结束 public static string EndDoSomeThing(IAsyncResult asyncResult) { DoSomethingAsyncResult result = asyncResult as DoSomethingAsyncResult; if (result != null) { return result.Result; } return null; }
//AsyncResult对象 public class DoSomethingAsyncResult : IAsyncResult { private AsyncCallback _asyncCallback; private AutoResetEvent _asyncWaitHandle;
public DoSomethingAsyncResult(AsyncCallback asyncCallback, object state) { AsyncState = state; _asyncCallback = asyncCallback; _asyncWaitHandle = new AutoResetEvent(false); }
//设置结果 public void SetComplete(string result) { Result = result; IsCompleted = true; if (_asyncCallback != null) { _asyncCallback(this); } _asyncWaitHandle.Set(); }
public string Result { get; private set; }
public object AsyncState { get; private set; }
public WaitHandle AsyncWaitHandle { get { return _asyncWaitHandle; } }
public bool CompletedSynchronously { get { return false; } }
public bool IsCompleted { get; private set; } } }
复制代码

    本例中,采用定时器触发完成动作,实际中,可以在需要的时候触发完成。

    对于BeginXXX/EndXXX模式,调用BeginXXX表示开始一个异步方法,前面的参数表示方法所需的参数(可无),倒数第二个参数为回调方法(可空),最后一个参数用于穿越整个过程的相关对象(可空)。返回的IAsyncResult存储了异步方法的相关状态信息,一般来说我们自己的异步方法需要一个实现了该接口的类,类中包含了回调方法、等待对象、相关参数与结果等。

    异步方法的协作有三种方法,第一种,通过轮询 IsCompleted 属性,直到为true时,触发完成动作。

    第二种,通过回调方法,当异步方法完成时,由异步方法调用回调方法。

  

    第三种,通过WaitHandler等待异步方法完成,当异步方法完成时,由异步方法发出完成信号,使等待结束。

   

    当我们需要将多个异步方法包装成一个异步方法时,方法内部将充斥着大量的回调方法。

    类似于这样:

复制代码
public static IAsyncResult BeginDoSomeThing(int arg1, AsyncCallback callback, object state) {     var asyncResult = new DoSomethingAsyncResult(callback, state);
Timer timer = null; timer = new Timer(new TimerCallback(s => { timer.Dispose();
using (var fs = new FileStream("Data.dat", FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, 4096, FileOptions.Asynchronous)) { var writeresult = fs.BeginWrite(new byte[100], 0, 100, new AsyncCallback(wresult => { fs.EndWrite(wresult);
asyncResult.SetComplete("Finished"); }), null); } }), state, TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(2));
return asyncResult; }
复制代码

    呼~幸好还有匿名方法与闭包,否则将是一件多么恐怖的事啊。  

 

    新的异步模型

 

 

    我们清醒的看到,当需要多个异步方法需要协作时,代码将显得十分复杂,无法表现清晰的逻辑,于是,我们需要一个更好的异步模型。

    从.NET4开始,引入了新的异步模型。

    首先引入一个新概念:Task。

    Task代表一个可以被执行的任务,我们可以让他运行,关联其他任务,等待他,获取他的结果。值得注意的是,这里的Task可以是一个异步的任务,也可以是同步的任务,在没有特别说明的情况下都指异步任务。而返回一个Task对象的方法,我们一般认为这是一个异步方法。new Task或者Task.Run将生成一个在线程池中运行的异步任务。

    的按照惯例,我们看一下如何把一个普通的方法异步执行。

复制代码
public static void Main() {     var t1 = Task<int>.Run(() =>     {         Thread.Sleep(2000);         return 100;     }).ContinueWith(new Action<Task<int>>(t =>         {             Console.WriteLine(t.Result);         }));     t1.Wait(); }
复制代码

    对于类库中提供的异步方法,也有了新版本,XXXAsync。

复制代码
public static void Main() {     using (var fs = new FileStream("Data.dat", FileMode.OpenOrCreate, FileAccess.Write, FileShare.None, 4096,         FileOptions.Asynchronous))     {         var task = fs.WriteAsync(new byte[100], 0, 100)             .ContinueWith(new Action<Task>(t => {                 Console.WriteLine("Finished");             }));         task.Wait();     } }
复制代码

    我们不再关心如何去开始,何时会结束,一切变成了一些有关或无关的任务。

    让我们自己写一个异步方法吧。

复制代码
public static Task<string> DoSomethingAsync(int value) {     return Task<string>.Run(() =>          {             Thread.Sleep(2000);             return value.ToString();         }); ; }
复制代码

    好吧,你肯定是以我在偷懒,为什么不像BeginXXX/EndXXX一样从底层开始实现一个呢,那是因为Task的封装比较严,我们无法直接对其扩展。为了达到获取一个Task,在需要的时候设置完成与结果,可以借助 AsyncTaskMethodBuilder 来实现。

复制代码
public class ProgramClass {     public static void Main()     {         var task = ProcessAsync();         task.Wait();         var r = task.Result;     }
static Task<string> ProcessAsync() { //辅助工具 AsyncTaskMethodBuilder<string> builder = AsyncTaskMethodBuilder<string>.Create(); Timer timer = null; timer = new Timer(s => { timer.Dispose(); builder.SetResult("Finished");//在需要时设置结果 }, null, TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(2)); return builder.Task;//获取需要的Task } }
复制代码

    类似的方法,我们封装一个由多个异步方法组合成的异步方法。   

复制代码
public class ProgramClass {     public static void Main()     {         var task = ProcessAsync();         task.Wait();         var r = task.Result;     }
static Task<string> ProcessAsync() { //辅助工具 AsyncTaskMethodBuilder<string> builder = AsyncTaskMethodBuilder<string>.Create();
DoSomethingAync1().GetAwaiter().OnCompleted(() => { DoSomethingAync2().GetAwaiter().OnCompleted(() => { DoSomethingAync2().GetAwaiter().OnCompleted(() => { builder.SetResult("Finished"); }); }); });
return builder.Task;//获取需要的Task }
static Task<string> DoSomethingAync1() { ... } static Task<string> DoSomethingAync2() { ... } static Task<string> DoSomethingAync3() { ... } }
复制代码

    组合异步方法调用后,按顺序调用第一个异步方法,紧接着,产生需要的结果Task后返回。异步方法完成时回调指定的方法,并按顺序继续调用,所有方法完成后,把运行的最终结果设置给结果Task,那么整个任务即完成。 

  

    如果异步方法有回返值,那么组合的异步方法看上去会复杂一点。

复制代码
static Task<string> ProcessAsync() {     //辅助工具    AsyncTaskMethodBuilder<string> builder = AsyncTaskMethodBuilder<string>.Create();
string r1, r2, r3;//用于存储每一个任务的结构 var awaitor1 = DoSomethingAync1().GetAwaiter(); awaitor1.OnCompleted(() => { r1 = awaitor1.GetResult(); var awaitor2 = DoSomethingAync2().GetAwaiter(); awaitor2.OnCompleted(() => { r2 = awaitor2.GetResult(); var awaitor3 = DoSomethingAync3().GetAwaiter(); awaitor3.OnCompleted(() => { r3 = awaitor3.GetResult(); builder.SetResult(r1 + r2 + r3);//计算最终结构并设置结果 }); }); });
return builder.Task;//获取需要的Task}
复制代码

    代码虽然复杂了一点,但还能够接受,这里的每个异步方法的返回值需要临时变量来存储,包括每个异步方法的TaskAwaiter对象,需要跨越多个方法,这里将形成闭包,使得这些对象无法尽快释放,同时,每一个异步方法都将附加一个OnComplete回访方法的委托对象,这些都是使用上述方法的代价,这些代价在理论上是可以被优化的,但是带来的是更为复杂的代码结果,暂且放下吧,因为,解决方案就在后面。

 

 

    重口味语法糖

 

 

 在C#5中,添加了 async/await 关键字,使得上面遗留的问题得以解决,而且重点是,用起来非常简单!

    上面的代码在C#5时代可以写成下面的样子:

复制代码
static async Task<string> ProcessAsync() {     var r1 = await DoSomethingAync1();     var r2 = await DoSomethingAync2();     var r3 = await DoSomethingAync3();
return r1 + r2 + r3; }
复制代码

    是不是震惊了。

    他几乎和同步方法写法一致。程序的逻辑完全没有因为异步而打乱,并且减少了代码量,这就是语法糖的魅力。

    语法糖的背后隐藏了不为人知的内部实现,特别重口味语法糖,我们需要知道他背后的实现,才不致于消化不良。

    先看一下语法,async关键字告诉编译器,对本方法使用语法糖,对于这类方法只能返回 void/Task/Task<T>,返回void/Task代表异步方法不返回任何结果,返回void在调用方看来是一个同步方法,没有机会获取异步回调。返回Task<T>代表返回结果为T的异步方法,在该方法内部,可以直接返回类型为T的结果。在返回结果前可以使用await关键字调用其他异步方法,并且可以直接获取该异步方法的返回值。无需处理任何Task相关的内容。当async方法内部没有任何await时,该方法效果与同步相同,仅仅是简单包装后Task而已。

    该方法的执行顺序与前面我们自己实现的相同,内部实现也有一些类似,同样采用AsyncTaskBuilder构建Task对象,在我们自己实现的方法中,在方法内部(一个或多个匿名方法与闭包对象)实现多个异步方法的调度,而async/await语法糖则采用一个状态机对象作为媒介进行多个异步方法的调度。

  

    编译后,async异步方法将执行过程委托给状态机,自己则向AsyncTaskBuilder获取Task返回,状态机内部存储方法内部参与计算的临时变量(闭包),维护当前执行状态,-1代表开始与中间状态,-2代表结束,0-n代表正在执行第n个异步方法,状态机的MoveNext方法按顺序去调用其他的异步方法,如异步方法已执行完毕则继续往下执行,如未完毕,则设置当前状态,存储任务的Awaiter对象,并关联完成动作(状态机方法本身的单例委托对象)后结束,当异步方法执行完毕,继续调用状态机MoveNext方法,按照状态找到执行入口点,找到上次执行的Awaiter对象,并获取执行结果,然后继续找到下一个异步方法执行,重复以上的步骤,如果异步方法间有其他代码,照本执行,当所有异步方法与内部代码执行完毕后,通过AsyncTaskBuilder向异步方法的结果Task设置结果值,该Task即完成。

   

    从编译后的结果可以看到,这里不再存在闭包对象与多个回调方法及其委托对象,全部合在状态机对象当中,而每一次异步方法调用后的Awaiter对象也可以在异步方法完成后释放引用,在状态机对象中根据签名的种类提供必要的字段位置,状态机本身也是结构体,最大限度上减少了空间的开销与GC的压力,而所有的这一切,编译器通通搞定,而程序员,只需要关注逻辑的顺序与结果的处理即可。

    前一篇:详解 .NET 异步

 

    在前文中,介绍了.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 string ClientID     {         get;         private set;     }
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; } }
复制代码

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

复制代码
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包装多个异步方法),就显得十分方便。

posted @ 2012-05-10 16:12  one light  阅读(437)  评论(0编辑  收藏  举报