ASP.NET异步处理

前一篇:详解 .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:2TId:5   App:22288629    Begin:55:30,3176        End:55:40,3212  TPool:2TId:7   App:12549444    Begin:55:31,0426        End:55:41,0432  TPool:3TId:8   App:22008501    Begin:55:31,5747        End:55:41,5752  TPool:4TId:9   App:37121646    Begin:55:32,1067        End:55:42,1073  TPool:5TId:10  App:33156464    Begin:55:32,6387        End:55:42,6393  TPool:6TId:11  App:7995840     Begin:55:33,1707        End:55:43,1713  TPool:7TId:12  App:36610825    Begin:55:33,7028        End:55:43,7033  TPool:8TId:13  App:20554616    Begin:55:34,2048        End:55:44,2054  TPool:9TId:14  App:15510466    Begin:55:35,2069        End:55:45,2074  TPool:10TId:15  App:23324256    Begin:55:36,2049        End:55:46,2055  TPool:11TId:16  App:34250480    Begin:55:37,2050        End:55:47,2055  TPool:12TId:17  App:58408916    Begin:55:38,2050        End:55:48,2056  TPool:13TId:18  App:2348279     Begin:55:39,2051        End:55:49,2057  TPool:14TId:19  App:61669314    Begin:55:40,2051        End:55:50,2057  TPool:15TId:6   App:35898671    Begin:55:40,3212        End:55:50,3217  TPool:15TId:5   App:22288629    Begin:55:40,3232        End:55:50,3237  TPool:15TId:7   App:12549444    Begin:55:41,0432        End:55:51,0438  TPool:15TId:8   App:22008501    Begin:55:41,5752        End:55:51,5758  TPool:15TId:9   App:37121646    Begin:55:42,1073        End:55:52,1078  TPool:15TId:10  App:33156464    Begin:55:42,6393        End:55:52,6399  TPool:15TId:11  App:7995840     Begin:55:43,1713        End:55:53,1719  TPool:15TId:12  App:36610825    Begin:55:43,7043        End:55:53,7049  TPool:15TId:13  App:20554616    Begin:55:44,2054        End:55:54,2059  TPool:15TId:20  App:36865354    Begin:55:45,2074        End:55:55,2080  TPool:16TId:14  App:15510466    Begin:55:45,2084        End:55:55,2090  TPool:16TId:21  App:3196068     Begin:55:46,2055        End:55:56,2061  TPool:17TId:15  App:23324256    Begin:55:46,2065        End:55:56,2071  TPool:17TId:22  App:4186222     Begin:55:47,2055        End:55:57,2061  TPool:18TId:16  App:34250480    Begin:55:47,2065        End:55:57,2071  TPool:18TId:23  App:764807      Begin:55:48,2046        End:55:58,2052  TPool:19TId:17  App:58408916    Begin:55:48,2056        End:55:58,2062  TPool:19TId:24  App:10479095    Begin:55:49,2047        End:55:59,2052  TPool:20TId:18  App:2348279     Begin:55:49,2057        End:55:59,2062  TPool:20TId:25  App:4684807     Begin:55:50,2047        End:56:00,2053  TPool:21TId:19  App:61669314    Begin:55:50,2057        End:56:00,2063  TPool:21TId:6   App:35898671    Begin:55:50,3227        End:56:00,3233  TPool:21TId:5   App:22288629    Begin:55:50,3237        End:56:00,3243  TPool:21TId:7   App:12549444    Begin:55:51,0438        End:56:01,0443  TPool:21TId:8   App:22008501    Begin:55:51,5758        End:56:01,5764  TPool:21TId:9   App:37121646    Begin:55:52,1078        End:56:02,1084  TPool:21TId:10  App:33156464    Begin:55:52,6399        End:56:02,6404  TPool:21TId:11  App:7995840     Begin:55:53,1719        End:56:03,1725  TPool:21TId:26  App:41662089    Begin:55:53,7049        End:56:03,7055  TPool:22TId:12  App:36610825    Begin:55:53,7059        End:56:03,7065  TPool:22TId:13  App:20554616    Begin:55:54,2069        End:56:04,2075  TPool:22TId:27  App:46338128    Begin:55:55,2070        End:56:05,2076  TPool:23TId:14  App:15510466    Begin:55:55,2090        End:56:05,2096  TPool:23TId:20  App:36865354    Begin:55:55,2090        End:56:05,2096  TPool:23TId: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:2TId:5   App:37489757    Begin:41:07,9932        End:41:08,0032  TPool:2TId:5   App:44665200    Begin:41:08,0042        End:41:08,0142  TPool:2TId:6   App:37489757    Begin:41:08,0052        End:41:08,0152  TPool:2TId:5   App:44665200    Begin:41:08,0142        End:41:08,0242  TPool:2TId:6   App:37489757    Begin:41:08,0152        End:41:08,0252  TPool:2TId:5   App:44665200    Begin:41:08,0242        End:41:08,0342  TPool:2TId:6   App:37489757    Begin:41:08,0252        End:41:08,0352  TPool:2TId:5   App:44665200    Begin:41:08,0342        End:41:08,0442  TPool:2TId:6   App:37489757    Begin:41:08,0352        End:41:08,0452  TPool:2TId:5   App:44665200    Begin:41:08,0442        End:41:08,0542  TPool:2TId:6   App:37489757    Begin:41:08,0452        End:41:08,0552  TPool:2TId:5   App:44665200    Begin:41:08,0542        End:41:08,0642  TPool:2TId:6   App:37489757    Begin:41:08,0552        End:41:08,0652  TPool:2TId:5   App:44665200    Begin:41:08,0642        End:41:08,0742  TPool:2TId:6   App:37489757    Begin:41:08,0652        End:41:08,0752  TPool:2TId:5   App:44665200    Begin:41:08,0742        End:41:08,0842  TPool:2TId:6   App:37489757    Begin:41:08,0752        End:41:08,0852  TPool:2TId:5   App:44665200    Begin:41:08,0842        End:41:08,0942  TPool:2TId:6   App:37489757    Begin:41:08,0852        End:41:08,0952  TPool:2TId:5   App:44665200    Begin:41:08,0942        End:41:08,1042  TPool:2TId:6   App:37489757    Begin:41:08,0952        End:41:08,1052  TPool:2TId:5   App:44665200    Begin:41:08,1042        End:41:08,1142  TPool:2TId:6   App:37489757    Begin:41:08,1052        End:41:08,1152  TPool:2TId:5   App:44665200    Begin:41:08,1142        End:41:08,1242  TPool:2TId:6   App:37489757    Begin:41:08,1152        End:41:08,1252  TPool:2TId:5   App:44665200    Begin:41:08,1242        End:41:08,1342  TPool:2TId:6   App:37489757    Begin:41:08,1252        End:41:08,1352  TPool:2TId:5   App:44665200    Begin:41:08,1342        End:41:08,1442  TPool:2TId:6   App:37489757    Begin:41:08,1352        End:41:08,1452  TPool:2TId:5   App:44665200    Begin:41:08,1442        End:41:08,1542  TPool:2TId:6   App:37489757    Begin:41:08,1452        End:41:08,1552  TPool:2TId:5   App:44665200    Begin:41:08,1542        End:41:08,1642  TPool:2TId:6   App:37489757    Begin:41:08,1552        End:41:08,1652  TPool:2TId:5   App:44665200    Begin:41:08,1642        End:41:08,1742  TPool:2TId:6   App:37489757    Begin:41:08,1652        End:41:08,1752  TPool:2TId:5   App:44665200    Begin:41:08,1742        End:41:08,1842  TPool:3TId:7   App:12547953    Begin:41:08,1752        End:41:08,1852  TPool:3TId:6   App:37489757    Begin:41:08,1762        End:41:08,1862  TPool:3TId:5   App:44665200    Begin:41:08,1842        End:41:08,1942  TPool:3TId:7   App:12547953    Begin:41:08,1852        End:41:08,1952  TPool:3TId:6   App:37489757    Begin:41:08,1862        End:41:08,1962  TPool:3TId:5   App:44665200    Begin:41:08,1942        End:41:08,2042  TPool:3TId:7   App:12547953    Begin:41:08,1952        End:41:08,2092  TPool:3TId:6   App:37489757    Begin:41:08,1962        End:41:08,2102  TPool:3TId:5   App:44665200    Begin:41:08,2052        End:41:08,2152  TPool:3TId:7   App:12547953    Begin:41:08,2092        End:41:08,2192  TPool:3TId:6   App:37489757    Begin:41:08,2102        End:41:08,2202  TPool:3TId:5   App:44665200    Begin:41:08,2152        End:41:08,2252  TPool:3TId: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:5App:58766839    Begin:39:47,8358        ThreadId:5      End:39:52,8321  ThreadId:7App:23825510    Begin:39:47,8348        ThreadId:5      End:39:52,8321  ThreadId:7App:30480920    Begin:39:47,8348        ThreadId:5      End:39:52,8321  ThreadId:7App:62301924    Begin:39:47,8348        ThreadId:6      End:39:52,8321  ThreadId:6App:28062782    Begin:39:47,8338        ThreadId:5      End:39:52,8321  ThreadId:6App:41488021    Begin:39:47,8338        ThreadId:6      End:39:52,8321  ThreadId:7App:15315213    Begin:39:47,8338        ThreadId:6      End:39:52,8321  ThreadId:6App:17228638    Begin:39:47,8328        ThreadId:5      End:39:52,8321  ThreadId:7App:51438283    Begin:39:47,8328        ThreadId:6      End:39:52,8321  ThreadId:6App:32901400    Begin:39:47,8328        ThreadId:5      End:39:52,8321  ThreadId:7App:61925337    Begin:39:47,8358        ThreadId:6      End:39:52,8321  ThreadId:6App:24914721    Begin:39:47,8318        ThreadId:6      End:39:52,8321  ThreadId:6App:26314214    Begin:39:47,8318        ThreadId:6      End:39:52,8321  ThreadId:6App:51004322    Begin:39:47,8358        ThreadId:6      End:39:52,8321  ThreadId:6App:51484875    Begin:39:47,8308        ThreadId:5      End:39:52,8321  ThreadId:7App:19420176    Begin:39:47,8308        ThreadId:6      End:39:52,8321  ThreadId:6App:16868352    Begin:39:47,8298        ThreadId:6      End:39:52,8321  ThreadId:7App:61115195    Begin:39:47,8298        ThreadId:5      End:39:52,8321  ThreadId:6App:63062333    Begin:39:47,8288        ThreadId:6      End:39:52,8321  ThreadId:6App:53447344    Begin:39:47,8298        ThreadId:5      End:39:52,8321  ThreadId:7App:31665793    Begin:39:47,8288        ThreadId:5      End:39:52,8321  ThreadId:7App:2174563     Begin:39:47,8288        ThreadId:6      End:39:52,8321  ThreadId:6App:12053474    Begin:39:47,8318        ThreadId:5      End:39:52,8321  ThreadId:7App:41728762    Begin:39:47,8278        ThreadId:6      End:39:52,8321  ThreadId:6App:6385742     Begin:39:47,8278        ThreadId:5      End:39:52,8321  ThreadId:7App:13009416    Begin:39:47,8268        ThreadId:6      End:39:52,8321  ThreadId:6App:43205102    Begin:39:47,8268        ThreadId:5      End:39:52,8321  ThreadId:7App:14333193    Begin:39:47,8268        ThreadId:6      End:39:52,8321  ThreadId:6App:2808346     Begin:39:47,8258        ThreadId:6      End:39:52,8321  ThreadId:6App:37489757    Begin:39:47,8128        ThreadId:5      End:39:52,8231  ThreadId:6App:34106743    Begin:39:47,8258        ThreadId:5      End:39:52,8321  ThreadId:7App:30180123    Begin:39:47,8248        ThreadId:6      End:39:52,8321  ThreadId:6App:44313942    Begin:39:47,8248        ThreadId:5      End:39:52,8321  ThreadId:7App:12611187    Begin:39:47,8248        ThreadId:6      End:39:52,8321  ThreadId:6App:7141266     Begin:39:47,8238        ThreadId:5      End:39:52,8321  ThreadId:7App:25425822    Begin:39:47,8278        ThreadId:5      End:39:52,8321  ThreadId:7App:51288387    Begin:39:47,8238        ThreadId:5      End:39:52,8321  ThreadId:7App:66166301    Begin:39:47,8228        ThreadId:6      End:39:52,8321  ThreadId:6App:34678979    Begin:39:47,8228        ThreadId:6      End:39:52,8321  ThreadId:7App:10104599    Begin:39:47,8218        ThreadId:5      End:39:52,8321  ThreadId:6App:47362231    Begin:39:47,8258        ThreadId:5      End:39:52,8321  ThreadId:7App:40535505    Begin:39:47,8218        ThreadId:6      End:39:52,8321  ThreadId:7App:20726372    Begin:39:47,8368        ThreadId:5      End:39:52,8321  ThreadId:5App:2730334     Begin:39:47,8368        ThreadId:6      End:39:52,8321  ThreadId:6App:59884855    Begin:39:47,8368        ThreadId:5      End:39:52,8321  ThreadId:7App:39774547    Begin:39:47,8238        ThreadId:6      End:39:52,8321  ThreadId:6App:12070837    Begin:39:47,8378        ThreadId:6      End:39:52,8491  ThreadId:7App:64828693    Begin:39:47,8218        ThreadId:5      End:39:52,8331  ThreadId:6App: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包装多个异步方法),就显得十分方便。

时间: 2024-11-06 19:41:09

ASP.NET异步处理的相关文章

重新理解:ASP.NET 异步编程(转)

http://www.cnblogs.com/xishuai/p/asp-net-async-await-and-exception-handling.html 相关博文: 异步编程 In .NET(回味无穷!!!) ASP.NET sync over async(异步中同步,什么鬼?) 本来这篇博文想探讨下异步中的异常操作,但自己在做异步测试的时候,又对 ASP.NET 异步有了新的认识,可以说自己之前对异步的理解还是有些问题,先列一下这篇博文的三个解惑点: async await 到底是什么

ASP.NET 异步编程

ASP.NET 异步编程 相关博文: 异步编程 In .NET(回味无穷!!!) ASP.NET sync over async(异步中同步,什么鬼?) 本来这篇博文想探讨下异步中的异常操作,但自己在做异步测试的时候,又对 ASP.NET 异步有了新的认识,可以说自己之前对异步的理解还是有些问题,先列一下这篇博文的三个解惑点: async await 到底是什么鬼??? 异步操作中发生异常,该如何处理? 异步操作中发生异常(有无 catch throw 情况),Application_Error

详解 ASP.NET异步

在前文中,介绍了.NET下的多种异步的形式,在WEB程序中,天生就是多线程的,因此使用异步应该更为谨慎.本文将着重展开ASP.NET中的异步. [注意]本文中提到的异步指的是服务器端异步,而非客户端异步(Ajax). 对于HTTP的请求响应模型,服务器无法主动通知或回调客户端,当客户端发起一个请求后,必须保持连接等待服务器的返回结果,才能继续处理,因此,对于客户端来说,请求与响应是无法异步进行,也就是说无论服务器如何处理请求,对于客户端来说没有任何差别. 那么ASP.NET异步指的又是什么,解决

【1】ASP.NET异步(1)

图标说明了异步的基础认识. 1.如果没有Ajax,提交之后整个页会刷新(左图).右图所示的虚线范围区域加入了ajax技术,提交之后只更新了虚线区域的内容,这样看比较直白. <form>①<asp:ScriptManager...</asp:ScriptManager> //必须,用于连接Ajax js的连接;②<asp:UpdatePanel ...>③<ContentTemplate> ...具体内容...<ContentTemplate>

ASP.NET 异步Web API + jQuery Ajax 文件上传代码小析

该示例中实际上应用了 jquery ajax(web client) + async web api 双异步. jquery ajax post 1 $.ajax({ 2 type: "POST", 3 url: "/api/FileUpload", 4 contentType: false, 5 processData: false, 6 data: data, 7 success: function (results) { 8 ShowUploadControl

Asp.net 异步调用WebService

//服务代码 [WebMethod] public string Test(int sleepTimes, int val) { Thread.Sleep(sleepTimes); var log = AppDomain.CurrentDomain.BaseDirectory + "/logs/log.log"; using (var sw = new StreamWriter(log, true)) { sw.WriteLine(DateTime.Now.ToString() + &

初探asp.net异步编程

终于毕业了,也顺利进入一家期望的旅游互联网公司.27号入职.放肆了一个多月没写代码,好方啊. 写在前面(带着问题学习) 一.根据代码和执行结果,初探异步编程的执行过程. *问题1:await会让当前线程一直等待吗? *问题2:等待await数据返回交给等待线程再继续向下执行吗? *问题3:向await下一条语句执行的线程,是执行await的线程吗? 二.异步编程async,await的使用意义和适用场景. *问题1:异步编程可以让程序变快吗? *问题2:异步编程执行耗时计算有用吗? *问题3:既

Asp.Net异步编程-使用了异步,性能就提升了吗?

position:static(静态定位) 当position属性定义为static时,可以将元素定义为静态位置,所谓静态位置就是各个元素在HTML文档流中应有的位置 podisition定位问题.所以当没有定义position属性时,并不说明该元素没有自己的位置,它会遵循默认显示为静态位置,在静态定位状态下无法通过坐标值(top,left,right,bottom)来改变它的位置. position:absolute(绝对定位) 当position属性定义为absolute时,元素会脱离文档流

asp.net异步上传

界面如下: 需要用到MyAjaxForm.js库 <script src="~/Scripts/MyAjaxForm.js"></script>  @using (Ajax.BeginForm("AjaxAddUserPrintReport", "Admin", new AjaxOptions() { HttpMethod = "post", LoadingElementId = "loadi