ASP.NET MVC中的异步操作是否使用.NET 4上的ThreadPool中的线程

tug*_*erk 157 .net c# asp.net-mvc asynchronous asp.net-mvc-3

在这个问题之后,在ASP.NET MVC中使用异步操作时,我感到很舒服.所以,我写了两篇博文:

我对ASP.NET MVC上的异步操作有太多的误解.

我总是听到这句话:如果操作异步运行,应用程序可以更好地扩展

我也听到了很多这样的句子:如果你有大量的流量,你可能最好不要异步执行查询 - 消耗2个额外的线程来为一个请求提供服务会使资源远离其他传入的请求.

我认为这两句话是不一致的.

我没有太多关于threadpool如何在ASP.NET上工作的信息,但我知道线程池的线程大小有限.所以,第二句话必须与这个问题有关.

我想知道ASP.NET MVC中的异步操作是否在.NET 4上使用ThreadPool中的线程?

例如,当我们实现AsyncController时,应用程序结构如何?如果我获得巨大的流量,实现AsyncController是一个好主意吗?

有没有人可以在我眼前取下这个黑色的窗帘并向我解释ASP.NET MVC 3(NET 4)上有关异步的处理?

编辑:

我已经阅读了下面这个文件近几百次,我理解主要的交易,但我仍然感到困惑,因为那里有太多不一致的评论.

在ASP.NET MVC中使用异步控制器

编辑:

让我们假设我有如下控制器动作(AsyncController虽然不是实现):

public ViewResult Index() { 

    Task.Factory.StartNew(() => { 
        //Do an advanced looging here which takes a while
    });

    return View();
}
Run Code Online (Sandbox Code Playgroud)

正如你在这里看到的那样,我开始操作并忘掉它.然后,我立即返回而不等待它完成.

在这种情况下,这是否必须使用来自线程池的线程?如果是这样,在完成之后,该线程会发生什么?GC完成后会进来清理吗?

编辑:

对于@ Darin的回答,这里是一个与数据库对话的异步代码示例:

public class FooController : AsyncController {

    //EF 4.2 DbContext instance
    MyContext _context = new MyContext();

    public void IndexAsync() { 

        AsyncManager.OutstandingOperations.Increment(3);

        Task<IEnumerable<Foo>>.Factory.StartNew(() => { 

           return 
                _context.Foos;
        }).ContinueWith(t => {

            AsyncManager.Parameters["foos"] = t.Result;
            AsyncManager.OutstandingOperations.Decrement();
        });

        Task<IEnumerable<Bars>>.Factory.StartNew(() => { 

           return 
                _context.Bars;
        }).ContinueWith(t => {

            AsyncManager.Parameters["bars"] = t.Result;
            AsyncManager.OutstandingOperations.Decrement();
        });

        Task<IEnumerable<FooBar>>.Factory.StartNew(() => { 

           return 
                _context.FooBars;
        }).ContinueWith(t => {

            AsyncManager.Parameters["foobars"] = t.Result;
            AsyncManager.OutstandingOperations.Decrement();
        });
    }

    public ViewResult IndexCompleted(
        IEnumerable<Foo> foos, 
        IEnumerable<Bar> bars,
        IEnumerable<FooBar> foobars) {

        //Do the regular stuff and return

    }
}
Run Code Online (Sandbox Code Playgroud)

Dar*_*rov 175

这是一篇很好的文章,我建议你阅读以更好地理解ASP.NET中的异步处理(这是异步控制器基本上代表的).

我们首先考虑一个标准的同步动作:

public ActionResult Index()
{
    // some processing
    return View();
}
Run Code Online (Sandbox Code Playgroud)

当对此操作发出请求时,将从线程池中抽取一个线程,并在此线程上执行此操作的主体.因此,如果此操作内的处理速度很慢,则会阻止此线程进行整个处理,因此无法重用此线程来处理其他请求.在请求执行结束时,线程返回到线程池.

现在让我们举一个异步模式的例子:

public void IndexAsync()
{
    // perform some processing
}

public ActionResult IndexCompleted(object result)
{
    return View();
}
Run Code Online (Sandbox Code Playgroud)

当请求发送到Index操作时,将从线程池中抽取一个线程,并IndexAsync执行该方法的主体.一旦此方法的主体完成执行,该线程就会返回到线程池.然后,使用标准AsyncManager.OutstandingOperations,一旦发出异步操作的信号,就会从线程池中抽取另一个线程,并在其IndexCompleted上执行操作体,并将结果呈现给客户端.

因此,我们在此模式中可以看到,单个客户端HTTP请求可以由两个不同的线程执行.

现在有趣的部分发生在IndexAsync方法内部.如果你在其中有阻塞操作,那么你完全是在浪费异步控制器的全部目的,因为你阻塞了工作线程(请记住,这个动作的主体是在从线程池中抽取的线程上执行的).

那么我们什么时候才能真正利用异步控制器呢?

恕我直言,当我们进行I/O密集型操作(例如对远程服务的数据库和网络调用)时,我们可以获得最大收益.如果您有CPU密集型操作,异步操作不会给您带来太多好处.

那么为什么我们可以从I/O密集型操作中获益呢?因为我们可以使用I/O完成端口.IOCP非常强大,因为在执行整个操作期间不会占用服务器上的任何线程或资源.

他们是如何工作的?

假设我们要使用WebClient.DownloadStringAsync方法下载远程网页的内容.您调用此方法将在操作系统中注册IOCP并立即返回.在处理整个请求期间,服务器上不会消耗任何线程.一切都发生在远程服务器上.这可能需要很长时间,但你不在乎,因为你不会危及你的工作线程.一旦收到响应,就会发出IOCP信号,从线程池中抽取一个线程,并在该线程上执行回调.但正如您所看到的,在整个过程中,我们并没有垄断任何线程.

使用FileStream.BeginRead,SqlCommand.BeginExecute等方法也是如此.

如何并行化多个数据库调用?假设您有一个同步控制器操作,其中您按顺序执行了4个阻塞数据库调用.很容易计算出,如果每个数据库调用需要200ms,那么您的控制器操作将花费大约800ms来执行.

如果您不需要按顺序运行这些调用,那么将它们并行化可以提高性能吗?

这是个大问题,不容易回答.可能是,可能不是.它完全取决于您如何实现这些数据库调用.如果您使用异步控制器和I/O完成端口,如前所述,您将提高此控制器操作和其他操作的性能,因为您不会独占工作线程.

另一方面,如果你实现它们很差(在线程池的线程上执行阻塞数据库调用),你基本上会将执行此操作的总时间减少到大约200ms,但是你会消耗4个工作线程,所以你由于池中缺少线程来处理它们,可能会降低可能会变得饥饿的其他请求的性能.

所以这很困难,如果你不准备对你的应用程序进行大量的测试,不要实现异步控制器,因为你可能会造成更多的伤害而不是受益.只有在您有理由的情况下才能实现它们:例如,您已经确定标准同步控制器操作是应用程序的瓶颈(在执行大量负载测试和测量之后).

现在让我们考虑你的例子:

public ViewResult Index() { 

    Task.Factory.StartNew(() => { 
        //Do an advanced looging here which takes a while
    });

    return View();
}
Run Code Online (Sandbox Code Playgroud)

当收到索引操作的请求时,将从线程池中抽取一个线程来执行其主体,但是它的主体仅使用TPL调度新任务.因此,操作执行结束,线程返回到线程池.除此之外,TPL使用线程池中的线程来执行其处理.因此,即使原始线程返回到线程池,您也从该池中绘制了另一个线程来执行任务的主体.所以你已经危及你宝贵的游泳池中的2个线程.

现在让我们考虑以下内容:

public ViewResult Index() { 

    new Thread(() => { 
        //Do an advanced looging here which takes a while
    }).Start();

    return View();
}
Run Code Online (Sandbox Code Playgroud)

在这种情况下,我们手动生成一个线程.在这种情况下,执行索引操作的主体可能需要稍长的时间(因为生成新线程比从现有池中绘制一个更昂贵).但是高级日志记录操作的执行将在不属于池的线程上完成.因此,我们不会危害池中的线程,这些线程可以免费提供其他请求.

  • @tugberk,数据库调用是I/O操作,但它都取决于你如何实现它们.如果你使用阻塞数据库调用,如`SqlCommand.ExecuteReader`,你就是在浪费一切,因为这是一个阻塞调用.您正在阻塞执行此调用的线程,如果此线程恰好是池中的线程,则非常糟糕.只有在使用I/O完成端口时才会受益:`SqlCommand.BeginExecuteReader`.如果您不使用IOCP而不使用IOCP,请不要使用异步控制器,因为您将对应用程序的整体性能造成更大的损害. (10认同)
  • @tugberk,您正在并行运行它们,因此与按顺序运行它们相比,执行的总时间要少.但是为了运行它们,你使用工作线程.实际上,EF很懒,所以当你执行`_context.Foo`时,你实际上并没有执行任何操作.您只是构建表达式树.要非常小心.仅当您开始枚举结果集时,才会延迟查询执行.如果在视图中发生这种情况,这可能会对性能造成灾难性后果.为了急切地执行EF查询,最后添加`.ToList()`. (3认同)
  • @tugberk,您需要一个负载测试工具,以便在您的网站上并行模拟多个用户,以了解它在高负载下的行为.Mini Profiler无法模拟您网站上的负载.它可能有助于您查看和优化您的ADO.NET查询并分析单个请求,当您需要了解当您的网站在真实世界中遇到大量用户的行为时,该请求是无用的. (2认同)

K. *_*Bob 48

是的 - 所有线程都来自线程池.您的MVC应用程序已经是多线程的,当请求进入新线程时,将从池中获取并用于为请求提供服务.该线程将被"锁定"(来自其他请求),直到请求完全得到服务并完成.如果池中没有可用的线程,则请求必须等到一个可用.

如果您有异步控制器,他们仍然从池中获取一个线程,但在为请求提供服务时,他们可以放弃线程,同时等待某些事情发生(并且该线程可以被提供给另一个请求)以及当原始请求需要一个线程时再次从池中获得一个.

不同之处在于,如果您有很多长时间运行的请求(线程正在等待某些事情的响应),您可能会耗尽池中的线程甚至是基本请求.如果您有异步控制器,则您没有任何其他线程,但正在等待的那些线程将返回到池并可以为其他请求提供服务.

一个近乎真实的例子......想想就像坐公共汽车,有五个人等着上车,第一个上车,付钱和坐下(司机服务他们的请求),你上车(司机正在服务你的要求)但你找不到你的钱; 当你在口袋里摸索时,司机放弃了你并接下来的两个人(服务他们的请求),当你找到你的钱时,司机开始再次与你打交道(完成你的请求) - 第五个人必须等到你已经完成但是第三和第四个人在你获得服务的一半时获得了服务.这意味着驱动程序是来自池的唯一线程,乘客是请求.如果有两个司机但是你可以想象,它写起来如何运作太复杂了......

没有异步控制器,你身后的乘客在你寻找你的钱时必须等待很长时间,同时公交车司机也没有工作.

因此,结论是,如果很多人不知道他们的钱在哪里(即需要很长时间来响应驱动程序所要求的内容)异步控制器可以很好地帮助请求的吞吐量,从而加速某些过程.如果没有aysnc控制器,每个人都会等到前面的人被完全处理完毕.但是不要忘记,在MVC中,您在一条总线上有很多总线驱动程序,因此异步不是自动选择.

  • 非常好的比喻.谢谢. (6认同)

Mik*_*son 10

这里有两个概念.首先,我们可以使代码并行运行以更快地执行或在另一个线程上调度代码以避免让用户等待.你有的例子

public ViewResult Index() { 

    Task.Factory.StartNew(() => { 
        //Do an advanced looging here which takes a while
    });

    return View();
}
Run Code Online (Sandbox Code Playgroud)

属于第二类.用户将获得更快的响应,但服务器上的总工作负载更高,因为它必须执行相同的工作+处理线程.

另一个例子是:

public ViewResult Index() { 

    Task.Factory.StartNew(() => { 
        //Make async web request to twitter with WebClient.DownloadString()
    });

    Task.Factory.StartNew(() => { 
        //Make async web request to facebook with WebClient.DownloadString()
    });


    //wait for both to be ready and merge the results

    return View();
}
Run Code Online (Sandbox Code Playgroud)

因为请求并行运行,所以用户不必等待它们在串行中完成的时间.但是你应该意识到我们在这里消耗的资源多于串行运行的资源,因为我们在线程等待的同时在许多线程上运行代码.

这在客户端场景中完全没问题.在新任务中包含同步长时间运行的代码(在另一个线程上运行它)也很常见,同时保持ui响应或并行化以使其更快.但是线程仍然在整个持续时间内使用.在高负载的服务器上,这可能适得其反,因为您实际使用了更多资源.这是人们警告过你的

MVC中的异步控制器有另一个目标.这里的要点是避免让线程无所事事(这可能会影响可伸缩性).只有您调用的API具有异步方法才真正重要.像WebClient.DowloadStringAsync().

关键是你可以让你的线程返回来处理新的请求,直到web请求完成,它会调用你的回调获得相同或新的线程并完成请求.

我希望你理解异步和并行之间的区别.将并行代码视为线程所在的代码并等待结果.虽然异步代码是代码,在代码完成后您将收到通知,然后您可以重新开始工作,同时线程可以执行其他工作.


Pau*_*ner 6

如果操作异步运行,应用程序可以更好地扩展,但前提是有可用于服务其他操作的资源.

异步操作可确保您永远不会阻止某个操作,因为现有操作正在进行中.ASP.NET有一个异步模型,允许多个请求并行执行.可以将请求排队并将它们处理为FIFO,但是如果有数百个请求排队并且每个请求需要100ms来处理,这将无法很好地扩展.

如果你有交通量巨大,你可能会更好不是异步执行的查询,因为可能没有额外的资源来服务请求.如果没有备用资源,那么您的请求将被强制排队,占用指数更长或完全失败,在这种情况下,异步开销(互斥和上下文切换操作)不会给您任何东西.

就ASP.NET而言,你没有选择 - 它使用异步模型,因为这对服务器 - 客户端模型有意义.如果您要在内部编写自己的代码,使用异步模式来尝试更好地扩展,除非您尝试管理在所有请求之间共享的资源,否则您实际上看不到任何改进,因为它们已经被包装在异步过程中,不阻止任何其他事情.

最终,在您真正了解导致系统瓶颈的因素之前,这一切都是主观的.有时很明显异步模式会有所帮助(通过防止排队的资源阻塞).最终,仅测量和分析系统可以指示您可以在哪里获得效率.

编辑:

在您的示例中,Task.Factory.StartNew调用将在.NET线程池上排队操作.线程池线程的性质将被重用(以避免创建/销毁大量线程的成本).操作完成后,线程将被释放回池中以供另一个请求重用(垃圾收集器实际上不会涉及,除非您在操作中创建了一些对象,在这种情况下,它们按照正常情况收集作用域).

就ASP.NET而言,这里没有特殊操作.ASP.NET请求完成而不考虑异步任务.唯一的问题可能是你的线程池是否已经饱和(即现在没有线程可用于服务请求,并且池的设置不允许创建更多的线程),在这种情况下请求被阻止等待启动任务直到池线程可用.