Hello World
Spiga

Lab:体会ASP.NET异步处理请求的效果

2009-01-19 13:21 by 老赵, 10556 visits

关注我的朋友们一定记得,我不止一次强调过在ASP.NET应用程序中使用异步方式处理请求对于提高吞吐量的作用。不过似乎很多朋友们一直没有理解这样做的原因,亦或是对这样做的效果没有一个实际的“体会”,甚至在质疑这么做的功效。现在我将向大家进行一个演示,我们一起来看一下这么做的实际效果如何。

限制最大工作线程数量

对于ASP.NET 2.0应用程序来说,一个工作线程即为一个客户端请求的处理单位,如果所有工作线程被占完,那么站点就无法处理其他请求。使用异步方式处理请求是ASP.NET 2.0中新增的高级特性,它充分利用操作系统和CLR的功能,使得应用程序在等待IO-Bound操作完成时不会占用线程池中的工作线程(Worker Thread)。关于这一点,我曾经在《正确使用异步操作》一文中进行了较为详细的描述。在CLR 2.0 SP1之后,最大工作线程的数量变成了CPU数 * 250——不过不同托管环境(如IIS或SQL Server),均可有不同体现,而我们的试验很难占用如此多的工作线程,因此进行试验的第一步便是限制应用程序中最大工作线程的数量。在.NET应用程序中,可以通过ThreadPool.SetMaxThreads静态方法设置线程池中最大工作线程数量。

与此同时,我们还应该使用ThreadPool.SetMinThreads方法来设置线程池中“必须保留”的最小线程数量。该值默认为1,它意味着在初始情况下线程池中只保留1个线程。如果同时来访多个请求,那么线程池就必须创建额外的线程。线程池创建线程的最大速度为500毫秒一个,因为实际上一个线程的工作往往能够很快完成,这样线程就能够“复用”了。如果没有这个限制,那么线程池就可能在短时间内分配太多线程反而导致性能降低。当空闲时,线程池也会逐渐销毁线程,以避免系统维护太多线程而导致的多余开销。在我们的试验中,必须马上能够动用足够的工作线程来处理请求,否则就会把大量的时间耗费在等待线程创建上,降低了试验结果的代表性。

ThreadPool.Get/SetMaxThreads方法都会涉及到Complete I/O Port Threads这个值,它在我们试验中并不会影响什么。具体原因目前我也不清楚,原本以为它应该限制了异步IO的数据,但是实验下来却不然。

我们可以使用以下方法来修改线程池中最大及最小线程数量:

void SetThreads(int min, int max)
{
    int worker, io;

    ThreadPool.GetMaxThreads(out worker, out io);
    ThreadPool.SetMinThreads(min, io);
    ThreadPool.SetMaxThreads(max, io);
}

试验同步请求

我的测试环境为Windows Server 2008 x86 Enterprise Edition下的IIS 7。当然,这个试验在IIS 6中也能进行——不过,Vista下的IIS 7限制了10个并发连接数量,因此您无法在Vista下进行这个试验。

我们使用最为普通的工具来进行测试:Tinyget、Powershell以及perfmon。Tinyget是IIS Resource Toolkit中的工具之一,可以用于模拟数量不多的并发请求,常常用于重现一些简单并发环境下出现的问题。Powershell,我们主要是使用它的Measure-Command命令来测试执行一条Tinyget语句所消耗的时间。Measure-Command最简单的语法是Measure-Command {...},其中大括号里包含的是被测量的脚本。permon自然广为人知,我们主要用其来检测ASP.NET Applications\Requests Executing的值,它表示了同时执行请求的数量。

现在我们准备一个Sync.ashx,它将会访问数据库,并执行一个WAITFOR函数,其目的是停留3秒钟:

public class Sync : IHttpHandler
{
    public void ProcessRequest(HttpContext context)
    {
        using (SqlConnection conn = new SqlConnection("..."))
        {
            SqlCommand cmd = new SqlCommand("WAITFOR DELAY '00:00:03';", conn);
            conn.Open();

            cmd.ExecuteNonQuery();
        }

        context.Response.ContentType = "text/plain";
        context.Response.Write("Sync");
    }

    public bool IsReusable { get { return false; } }
}

将最大及最小工作线程数量设为10,20,30,分别执行以下脚本:

Measure-Command {.\tinyget -srv:localhost -uri:/Sync.ashx -threads:30 -loop:1}

tinyget命令threads参数表明同时使用多少个线程进行请求,而loop参数表明“每个线程”将请求多少次。试验结果如下:

Max Worker Threads 10 15 20
Max Request Executing 6 11 16
Execution Time (s) 15.14 9.10 6.13
permon Snapshot 10 10 10

从试验结果中我们可以发现,可同时执行的请求数比最大工作线程少4(思考题:另外4个在做什么呢?),而同时执行的请求的数量越多,执行所有请求所消耗的时间也在越小。这和我们之前的想法基本一致。

试验异步请求

构建一个异步Handler:

public class Async : IHttpHandler, IHttpAsyncHandler
{
    public void ProcessRequest(HttpContext context) { }

    public bool IsReusable { get { return false; } }

    private SqlConnection m_conn;
    private SqlCommand m_cmd;
    private HttpContext m_context;

    public IAsyncResult BeginProcessRequest(
        HttpContext context, AsyncCallback cb, object extraData)
    {
        this.m_context = context;
        this.m_conn = new SqlConnection("Data Source=...;...;Asynchronous Processing=true");
        this.m_cmd = new SqlCommand("WAITFOR DELAY '00:00:03';", this.m_conn);
        this.m_conn.Open();

        return this.m_cmd.BeginExecuteNonQuery(cb, extraData);
    }

    public void EndProcessRequest(IAsyncResult result)
    {
        this.m_cmd.EndExecuteNonQuery(result);
        this.m_conn.Dispose();

        this.m_context.Response.ContentType = "text/plain";
        this.m_context.Response.Write("Hello World");
    }
}

唯一可能值得提到的是,如果要对SQL Server进行异步数据访问,则必须在连接字符串里加上Asynchronous Processing标记。那么我们把最大和最小工作线程数量设为10个,并使用以下脚本进行测试:

Measure-Command {.\tinyget -srv:localhost -r:7788 -uri:/Sync.ashx -threads:30 -loop:1}
[System.Threading.Thread]::Sleep(2000)
Measure-Command {.\tinyget -srv:localhost -r:7788 -uri:/Async.ashx -threads:30 -loop:1}
[System.Threading.Thread]::Sleep(2000)
Measure-Command {.\tinyget -srv:localhost -r:7788 -uri:/Async.ashx -threads:40 -loop:1}
[System.Threading.Thread]::Sleep(2000)
Measure-Command {.\tinyget -srv:localhost -r:7788 -uri:/Async.ashx -threads:50 -loop:1}

上述脚本首先将同时发起30次同步请求,再发起三次异步请求,数目分别是30、40和50。试验结果如下:

Max Request Executing 6 30 40 50
Execution Time (s) 15.06 3.10 3.11 3.10
permon Snapshot 10

结果再明显不过了:应用程序还是只能每次处理6个同步请求,但是对于异步请求来说似乎就“丝毫不受限制”了。为了更好的说明问题,我们再进行最后一个试验。

降低最小线程数量

之前提过,最小线程数量代表了线程池中所维护的最少线程数量。线程池将会根据需要来创建或销毁线程。

我们现在将最小线程数量设为1,最大线程数量设为20,使用同时发起50个请求。试验结果如下:

Max Request Executing 9 50
Execution Time (s) 18.27 3.37
perfmon Snapshot min-sync min-async

对于同步请求,同时处理的请求数目从1开始以每秒两个的速度增长,最终受限于“保护机制”而停止在9个线程。而对于异步请求,则是瞬间飙升至50个——因为这样的请求不需要占用工作线程,自然无需等待线程慢慢分配了。

看了以上的试验,不知道您是否有所感受?不如您也在自己的机器上试试看呢?

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

45 条回复

  1. welshem
    *.*.*.*
    链接

    welshem 2009-01-19 13:25:00

    不错

  2. 张明海
    *.*.*.*
    链接

    张明海 2009-01-19 13:27:00

    沙发

  3. Such Cloud
    *.*.*.*
    链接

    Such Cloud 2009-01-19 13:35:00

    使用了异步 客户端有什么变化吗

  4. 韦恩卑鄙
    *.*.*.*
    链接

    韦恩卑鄙 2009-01-19 13:40:00

    @Such Cloud 完全没有
    这里是指服务器端 iis 把请求转向到 clr以后的操作

  5. xq.cheng
    *.*.*.*
    链接

    xq.cheng 2009-01-19 15:20:00

    有些东西不太懂,顶一下慢慢看

  6. 1184[未注册用户]
    *.*.*.*
    链接

    1184[未注册用户] 2009-01-19 15:31:00

    为何你的网页浏览时候老是提示有脚本错误呢?

  7. kkun
    *.*.*.*
    链接

    kkun 2009-01-19 15:34:00

    坐下来慢慢消化

  8. 老赵
    admin
    链接

    老赵 2009-01-19 15:53:00

    --引用--------------------------------------------------
    1184: 为何你的网页浏览时候老是提示有脚本错误呢?
    --------------------------------------------------------
    什么错误,我一直在阿?

  9. andy.wu
    *.*.*.*
    链接

    andy.wu 2009-01-19 18:14:00

    很好。

  10. 123321[未注册用户]
    *.*.*.*
    链接

    123321[未注册用户] 2009-01-19 19:30:00

    好文章.
    请问楼主能不能提供那几个软件的下载地址啊?

  11. 123321[未注册用户]
    *.*.*.*
    链接

    123321[未注册用户] 2009-01-19 20:11:00

    是perfmon吧,是不是笔误啊

  12. 老赵
    admin
    链接

    老赵 2009-01-19 23:24:00

    --引用--------------------------------------------------
    123321: 是perfmon吧,是不是笔误啊
    --------------------------------------------------------
    嗯,笔误

  13. 装配脑袋
    *.*.*.*
    链接

    装配脑袋 2009-01-20 08:09:00

    老赵介绍下怎么让大家一下子把自己的程序改成真正的异步方式呀?

  14. 老赵
    admin
    链接

    老赵 2009-01-20 09:11:00

    --引用--------------------------------------------------
    装配脑袋: 老赵介绍下怎么让大家一下子把自己的程序改成真正的异步方式呀?
    --------------------------------------------------------
    说实话,要“改”且“方便”,不容易啊,我正在想是否应该有这么一个辅助类库出来可以方便开发……

  15. 小鬼00[未注册用户]
    *.*.*.*
    链接

    小鬼00[未注册用户] 2009-01-20 09:38:00

    public bool IsReusable { get { return false; } }

    问一个题外话,这里的return false; 和return true;有什么区别?

  16. 老赵
    admin
    链接

    老赵 2009-01-20 09:49:00

    @小鬼00
    是共享一个Handler处理多个请求,还是为每个请求使用不同的Handler

  17. 装配脑袋
    *.*.*.*
    链接

    装配脑袋 2009-01-20 10:08:00

    从前,记得有人告诉我古代的ASP.NET可以用Page.AddOnPreRenderCompleteAsync方法实现ASP.NET流水线中插入异步操作。不知道现代还能用否?

  18. 感動常在
    *.*.*.*
    链接

    感動常在 2009-01-20 10:09:00

    study

  19. 老赵
    admin
    链接

    老赵 2009-01-20 10:27:00

    @装配脑袋
    啥叫古代的asp.net,本来就是asp.net 2.0里才有的功能吧。
    现在当然也有。

  20. Ivan Jiang
    *.*.*.*
    链接

    Ivan Jiang 2009-01-20 10:59:00

    这个异步操作一定要在Handler处实现吗,能不能不改动Handler而直接放到数据层去呢?

    就以那个Article评论分页那实例。

    我们实际的应用程序大概是:

    1:前台页面用Ajax去请求一个“aspx”,"ashx"或Webservice页面,让他们去取数据。
    2:然后在取的数据访问层里面用异步操作提高性能(而不是在"ashx"上写数据访问或者异步的相关代码)

    可以这样吗?还是非得在Handler上继承IHttpHandler, IHttpAsyncHandler这些接口。

  21. 老赵
    admin
    链接

    老赵 2009-01-20 11:57:00

    @Ivan Jiang
    嗯,ASP.NET里面可以异步的东西很多。
    HttpHandler,HttpModule,Page都可以异步。
    当然关键还是HttpHandler,HttpModule
    Page是HttpHandler的一个实现。

  22. mervin[未注册用户]
    *.*.*.*
    链接

    mervin[未注册用户] 2009-01-20 17:18:00

    麻烦作者把字体调小写吧,看着太累了。。

  23. 老赵
    admin
    链接

    老赵 2009-01-20 17:19:00

    @mervin
    字体很大?你用的Text Size难道不是Medium?

  24. airwolf2026
    *.*.*.*
    链接

    airwolf2026 2009-01-21 09:00:00

    vista 里面的IIS7连接数应该是没有限制的吧,而是由于vista系统本身的tcp/ip连接数限制,间接造成IIS7连接数限制吧?
    前不久用xp和vista部署了一个相同站点,结果xp的没有几个人访问就报连接过多,而vista倒不会,系统日志倒是有警告说超过tcp/ip连接数限制...
    不知道是不是俺的错觉哈.

  25. 老赵
    admin
    链接

    老赵 2009-01-21 09:09:00

    @airwolf2026
    我不知道啊,我是看一些资料和测试,它们说vista下iis7有限制,忘了具体来源是什么了。

  26. Wencui
    *.*.*.*
    链接

    Wencui 2009-01-21 13:55:00

    Jeffrey的文章,顶一个!

    @airwolf2026
    不是IIS7的限制,是IE的限制。使用Tinyget的时候,我们需要将IE的最大连接数增大(改注册表),不然Tinyget不能模拟IE同时对同一个站点的发出许多请求。

    至于异步,Jeffrey提到了两种异步 1)ASP.NET application pool中的thread 异步 2)数据库 connection pool 的异步。他们是不同的东西。个人认为,没有必要大量使用异步,只有如下一些情况:

    1. 长时间的IO操作。可以使用异步的handler/page。
    2. 长时间SQL操作。我们可以同时使用以上两种异步。

  27. 老赵
    admin
    链接

    老赵 2009-01-21 13:57:00

    @Wencui
    和IE有关?Tinyget不是用IE的吧。
    至于异步,其实只要
    1、长时间
    2、能利用到IOCP
    用异步肯定是好的。

  28. 准备放假[未注册用户]
    *.*.*.*
    链接

    准备放假[未注册用户] 2009-01-21 23:44:00

    对于数据库为主的应用,在数据库优化方面控制很好(绝大多数SQL语句执行时间较短)后,异步能明显提高吞吐量。目前痛苦的是经常发现数据查询时间长,导致线程堆积。

  29. 老赵
    admin
    链接

    老赵 2009-01-22 00:16:00

    @准备放假
    查询时间长更不能让它阻塞了啊,呵呵

  30. james-brook[未注册用户]
    *.*.*.*
    链接

    james-brook[未注册用户] 2009-01-23 14:05:00

    其他4个线程可能负责其他任务吧!总不会所有的线程来处理一个任务的。

  31. 老赵
    admin
    链接

    老赵 2009-01-23 14:09:00

    @james-brook
    我的问题就是,它们在负责什么任务?

  32. WizardWu
    *.*.*.*
    链接

    WizardWu 2009-01-24 18:25:00

    great~

  33. Ivan Jiang
    *.*.*.*
    链接

    Ivan Jiang 2009-02-06 23:54:00

    BlogEngine里面为了实现异步发送邮件只有一行代码(前三行是他以前的一个版本,Utils.SendMailMessage是它一个方法。):

    //ThreadStart threadStart = delegate { Utils.SendMailMessage(message); };
    //Thread thread = new Thread(threadStart);
    //thread.IsBackground = true;
    //thread.Start();
    ThreadPool.QueueUserWorkItem(delegate { Utils.SendMailMessage(message); });

    他的注释是:Sends the mail message asynchronously in another thread.

    老赵,怎么去分析它这种开启另一个线程来异步执行的做法,这样写道是很轻松。貌似我们如果要改进手头的程序,只要在现有的方法外套上一个ThreadPool.QueueUserWorkItem就行了。这样做有什么优劣吗?

  34. 老赵
    admin
    链接

    老赵 2009-02-07 00:12:00

    @Ivan Jiang
    它是使用了一个额外的工作线程来发邮件,这样不会阻塞调用线程,仅此而已。
    我说的方法是利用IOCP省下工作线程,也就是比如在请求数据库的时候,是不占任何工作线程的。

  35. Ivan Jiang
    *.*.*.*
    链接

    Ivan Jiang 2009-02-07 01:43:00

    明白,也就是说BE那样写的开销还是不小的是吧

  36. 老赵
    admin
    链接

    老赵 2009-02-07 02:26:00

    --引用--------------------------------------------------
    Ivan Jiang: 明白,也就是说BE那样写的开销还是不小的是吧
    --------------------------------------------------------
    他的目的本不就是为了节省,其实他也没法节省……

  37. Cat Chen
    *.*.*.*
    链接

    Cat Chen 2009-03-07 13:40:00

    其实做comet的时候,异步是最有必要的,必须把thread还给pool,然后等消息来了再获取一个thread执行下去。lighttpd的优势在于此,lighttpd难以调试也因为这个。

  38. kaabe
    122.224.125.*
    链接

    kaabe 2010-12-07 13:51:03

    SetThreads(1, 10)

    在那里设置比较好?

    Global.Application_Start or Page_Load ?

  39. 快乐乔巴
    122.1.99.*
    链接

    快乐乔巴 2012-04-20 16:33:06

    老赵 你好 我在ashx里测试了下 第一次还行 刷新一下就一直在等待 我只写了一行代码ThreadPool.SetMaxThreads(10, 100); 就是改最大值为10. 写在ashx里或者aspx里 第一次行 第二次就一直等待 如果改成和100 就是和原来默认最大一样又好了。。。 不知道什么原因 如果这段代码写到ConsoleApplication程序里就没有任何问题 很困惑 求帮助

  40. 快乐乔巴
    122.1.99.*
    链接

    快乐乔巴 2012-04-20 17:07:56

    赵老师 我发现原因了 我是在Visual Studio2010上测试的 所以就出现这样的问题 如果我把web程序放到IIS上就没有问题了... 晕了哦

  41. 快乐乔巴
    122.1.99.*
    链接

    快乐乔巴 2012-04-20 17:23:04

    还有如果发布到IIS上之后 文章说同时执行的请求数比最大工作线程少4 在VS上测试的确少了4个 但是放到IIS上就只少了1个.. 设置最大10 出来workerThreads是9 又糊涂了... help me T_T 对了 我的IIS版本是7.5吧 OS是windows 7 VS2010

  42. 老赵
    admin
    链接

    老赵 2012-04-21 09:52:38

    @快乐乔巴

    一些细节方面的数字可能会根据不同版本服务器或运行时的默认配置有变化吧,理解就好,不必强制对应数字……

  43. 快乐乔巴
    122.1.99.*
    链接

    快乐乔巴 2012-04-23 12:45:23

    恩 我再研究研究 谢谢回复

  44. 快乐乔巴
    122.1.99.*
    链接

    快乐乔巴 2012-04-24 12:03:36

    赵老师 我还有好几个疑问困惑着我 下面是我的理解 不是对不对 我最初去理解线程池的时候并不是web页面上,而是在WindowsConsoleAPP下测试的 1.测试了下ConsoleAPP下 最初设置最大最小线程都为100
    循环 ThreadPool.QueueUserWorkItem(new WaitCallback(CallBack), now); 50次 WaitCallback中睡眠了5秒 最后结果只用了5秒 似乎是瞬间提高到50个线程 就像异步线程一样 但是我认为应为是工作线程 所以实质上还是同步的 由于同时50次访问的时候 因为最小设置了100 线程池里也同时并发50个线程 所以最后是结果只用5秒 同时我也输出了线程池中工作线程和I/O线程使用情况 可以看出使用的都是工作线程 这是我最初对线程池的理解 不是对不对

    现在测试Web程序的ashx,我就彻底迷茫了
    2.测试ashx 最初在Global.asax中也设置最大最小线程都为100 这里的是普通Handler,不是异步的 先测试这种 直接在测试的ashx文件里直接写Thread.Sleep(5000) 睡5秒 在这里要说下我的理解 每次访问ashx一次 调用的还是线程池里线程,我在ashx里也输出了工作线程的使用情况 的确如此,所以我就直接写睡了5秒 然后用你的工具测试了下 发起50个线程同时访问 结果时间居然用了18秒左右 第二次是11秒趋于稳定, 这里我就不是很明白了 不是已经设置最大最小100了吗? 照理说应该和ConsoleAPP那个程序一样啊 同时50个线程发起访问 线程池里也应该同时调用50个线程处理 不是吗? 而且我也输出了下程池中工作线程和I/O线程使用情况 用的也是工作线程 但是具体测试跟踪后 线程池不是一下子使用50个 而是(11秒的那个测试)先起20多个 然后慢慢的后面再起时几个 这是什么原因啊???

    3.现在轮到异步Handle了 这个测试倒是没问题 差不多是5秒左右 可是看工作线程使用情况 也是使用工作线程 I/O线程一直没变 异步线程调用难道不是调用I/O线程?? 昏昏..

    现在彻底昏了... 估计我太笨了 赵老师 再点拨我下吧 我肯定那块理解错了
    最好赵老师有时间能 跟我聊聊 用GMail的聊天也行 我的gmail是chopper7278@gmail.com

  45. 快乐乔巴
    122.1.99.*
    链接

    快乐乔巴 2012-04-26 16:04:11

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我