25

(这可能重复了问题ASP.NET MVC4 Async controller - Why to use?,但是关于 webapi,我不同意那里的答案)

假设我有一个长时间运行的 SQL 请求。它的数据应该被序列化为 JSON 并发送到浏览器(作为 xhr 请求的响应)。示例代码:

public class DataController : ApiController
{
    public Task<Data> Get()
    {
        return LoadDataAsync(); // Load data asynchronously?
    }
}

当我执行 $.getJson('api/data', ...) 时实际会发生什么(请参阅此海报http://www.asp.net/posters/web-api/ASP.NET-Web-API-Poster。 pdf ):

  1. [IIS] 请求被 IIS 接受。
  2. [IIS] IIS 等待托管池 ( http://msdn.microsoft.com/en-us/library/0ka9477y(v=vs.110).aspx ) 中的一个线程 [THREAD] 并开始在其中工作。
  3. [线程] Webapi 在该线程和其他类中创建新的 DataController 对象。
  4. [THREAD] 使用任务并行库在 [THREAD2] 中启动 sql-query
  5. [THREAD] 返回托管池,准备进行其他处理
  6. [THREAD2] 与 sql 驱动程序一起工作,在准备好数据时读取数据并调用 [THREAD3] 来回复 xhr 请求
  7. [THREAD3] 发送响应。

请随时纠正我,如果有什么问题。

他们说,在上面的问题中,重点和好处是,[THREAD2] 不是来自托管池,但是 MSDN 文章(上面的链接)说

默认情况下,并行库类型喜欢TaskTask<TResult>使用线程池线程来运行任务。

所以我得出一个结论,所有三个线程都来自托管池。

此外,如果我使用同步方法,我仍然会保持我的服务器响应,只使用一个线程(来自宝贵的线程池)。

那么,从 1 个线程交换到 3 个线程的实际意义是什么?为什么不只是最大化线程池中的线程呢?

是否有任何使用异步控制器的明显有用的方法?

4

4 回答 4

29

我认为关键的误解是围绕async任务的工作方式。我的博客上有一个async介绍,可能会有所帮助。

特别是,方法Task返回的aasync不会运行任何代码。相反,它只是通知调用者该方法的结果的一种便捷方式。您引用的 MSDN 文档仅适用于实际运行代码的任务,例如Task.Run.

顺便说一句,您引用的海报与线程无关。async以下是数据库请求中发生的情况(略微简化):

  1. 请求被 IIS 接受并传递给 ASP.NET。
  2. ASP.NET 采用其线程池线程之一并将其分配给该请求。
  3. WebApi 创建DataController等。
  4. 控制器操作启动异步 SQL 查询。
  5. 请求线程返回线程池。现在没有线程处理请求。
  6. 当结果从 SQL 服务器到达时,一个线程池线程读取响应。
  7. 该线程池线程通知请求它已准备好继续处理。
  8. 由于 ASP.NET 知道没有其他线程正在处理该请求,因此它只是将请求分配给同一个线程,以便它可以直接完成它。

如果你想要一些概念验证代码,我有一个旧的 Gist,它人为地将 ASP.NET 线程池限制为核心数(这是它的最小设置),然后执行 N+1 同步和异步请求。该代码只是延迟一秒钟,而不是联系 SQL 服务器,但一般原则是相同的。

于 2013-11-12T19:15:20.767 回答
4

异步操作的好处是,当控制器在等待 sql 查询完成时,没有为这个请求分配线程,而如果你使用同步方法,一个线程会在这个方法的执行过程中从头到尾被锁定那个方法。当 SQL 服务器在做它的工作时,线程除了等待什么也不做。如果您使用异步方法,则此同一线程可以在 SQL Server 执行其操作时响应其他请求。

我相信您的步骤在第 4 步是错误的,我认为它不会创建一个新线程来执行 SQL 查询。在 6 处没有创建新线程,它只是一个可用线程,用于从第一个线程停止的地方继续。6 处的线程可能与启动异步操作相同。

于 2013-11-12T18:27:11.640 回答
3

我认为以下描述了异步控制器相对于同步控制器的明显优势。

使用同步方法来服务高延迟调用的 Web 应用程序,其中线程池增长到 .NET 4.5 默认最大值 5,000 个线程将消耗大约 5 GB 的内存,而不是能够使用异步方法为相同请求提供服务的应用程序,并且只有 50线程。当您进行异步工作时,您并不总是使用线程。例如,当您发出异步 Web 服务请求时,ASP.NET 不会在异步方法调用和等待之间使用任何线程。使用线程池为具有高延迟的请求提供服务可能会导致大量内存占用和服务器硬件利用率低下。

来自在 ASP.NET MVC 4 中使用异步方法

于 2013-11-12T18:36:54.383 回答
3

异步的重点不是让应用程序多线程,而是让单线程应用程序继续执行不同的操作,而不是等待在不同线程或进程上执行的外部调用的响应。

考虑一个桌面应用程序,它显示来自不同交易所的股票价格。应用程序需要进行几次 REST/http 调用以从每个远程证券交易所服务器获取一些数据。

单线程应用程序会进行第一次调用,等待它得到第一组价格,更新它的窗口,然后调用下一个外部股票价格服务器,再次等待它得到价格,更新它窗户……等等……

我们可以让所有多线程并行启动请求并并行更新屏幕,但是由于大部分时间都花在等待来自远程服务器的响应上,这似乎有点过分了。

线程可能会更好:向第一个服务器发出请求,但不要等待答案留下一个标记,一个在价格到达时返回并继续发出第二个请求的地方,再次留下一个标记回到……等等的地方。

当所有请求都发出后,应用程序执行线程可以继续处理用户输入或任何需要的内容。

现在,当收到来自其中一个服务器的响应时,可以引导线程从之前放置的标记继续并更新窗口。

以上所有内容都可以长手编码,单线程,但是多线程通常更容易。现在,当我们编写 async/await 时,离开标记并返回的过程由编译器完成。所有单线程。

这里有两个关键点:

1)多线程仍然发生!我们对股票价格的请求的处理发生在不同的线程上(在不同的机器上)。如果我们进行数据库访问,情况也是如此。在等待计时器的示例中,计时器在不同的线程上运行。我们的应用程序虽然是单线程的,但执行点只是在外部线程执行时(以受控方式)跳跃

2) 一旦应用程序需要异步操作来完成,我们就会失去异步执行的好处。考虑一个显示来自两个交易所的咖啡价格的应用程序,该应用程序可以启动请求并在单个线程上异步更新它的窗口,但现在如果应用程序还计算两个交易所之间的价格差异,它就必须等待异步调用完成。这是强加给我们的,因为异步方法(例如,我们可能编写调用股票价格交易所的方法)不会返回股票价格,而是返回一个任务,这可以被认为是返回标记的一种方式被设置,所以函数可以完成并返回股票价格。

这意味着每个调用异步函数的函数都需要异步或者等待调用栈底部的“其他线程/进程/机器”调用完成,如果我们正在等待底部调用完成为什么要打扰异步呢?

当编写 web api、IIS 或其他主机是桌面应用程序时,我们异步编写控制器方法,以便主机可以在我们的线程上执行其他方法来服务其他请求,而我们的代码正在等待来自不同线程上的工作的响应/进程/机器。

于 2014-10-09T13:47:16.183 回答