前一篇:详解 .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(); }
        }
相关推荐
君无忧≈4 小时前
.NET生态下Native AOT兼容的Cron任务调度框架
.net
步步为营DotNet4 小时前
运用.NET Aspire 和 Native AOT 打造极致性能的云原生微服务
微服务·云原生·.net
꒰ঌ 安卓开发໒꒱21 小时前
.NET CAP入门到入土
后端·c#·.net
海盗12341 天前
.NET编码规范03-C#命名规范大全
linux·服务器·.net
SEO-狼术2 天前
GitKraken Desktop Crack
.net
拼搏的水杯2 天前
使用Lucene.Net实现全文检索
全文检索·.net·lucene
随心(cracker)3 天前
BeaverX Admin:一套真正能跑起来的开源管理后台
vue·.net·管理后台
忧郁的紫菜3 天前
异步编程 In .NET
.net
Denuin多啦咧梦3 天前
玩转 .NET 依赖注入:Microsoft.Extensions.DependencyInjection 深度指南
c#·.net·依赖注入