ApiController的异步方法 - 有什么好处?何时使用?

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. [THREAD] Webapi在该线程中创建新的DataController对象和其他类。
  4. [THREAD] 使用任务并行库在[THREAD2]中启动SQL查询。
  5. [THREAD] 返回托管池,准备进行其他处理。
  6. [THREAD2] 使用SQL驱动程序,读取数据并在准备就绪时调用[THREAD3]以回复xhr请求。
  7. [THREAD3] 发送响应。

如果有错误,请随时纠正我。

在上述问题中,他们说,要点和利润在于[THREAD2]不是来自托管池,然而MSDN文章(以上链接)说:
“默认情况下,并行库类型如 TaskTask<TResult> 使用线程池线程运行任务。”
所以我得出结论,所有三个线程都来自托管池。
此外,如果我使用同步方法,我仍然可以保持服务器响应,只使用一个线程(来自宝贵的线程池)。
那么,从1个线程切换到3个线程的实际意义是什么?为什么不仅仅最大化线程池中的线程数?
还有没有明显有用的使用异步控制器的方法?
4个回答

29
我认为关键的误解在于如何处理异步任务。我在我的博客上有一个async intro可能会有所帮助。
特别地,由async方法返回的Task不会运行任何代码。相反,它只是一种方便的方式来通知调用者该方法的结果。您引用的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服务器联系,但一般原则相同。


1
当SQL服务器返回结果时,线程池中的一个线程读取响应。您能详细说明一下吗?结果从哪里到达?谁会从线程池中调用线程? - Rustem Mustafin
2
假设您的SQL连接使用TCP/IP,结果将作为网络数据包到达。这会触发一个中断,设备驱动程序读取数据包并将其传递到用户模式。IOCP机制通知线程池套接字读取已完成,并确保响应完整、解析它,然后通知请求准备好继续执行。(这仍然略有简化)。 - Stephen Cleary
1
我特别关注“通知线程池...”。据我所知,当某些东西可以被通知时,它要么正在等待通知,要么定期检查通知队列。我得出结论,有1个或更多专用线程执行此操作。这是正确的吗?我们应该将该线程计算为“因异步而浪费”的资源吗? - Rustem Mustafin
2
不完全是这样,因为IOCP线程是共享的,仅在短时间内使用,即使您不执行异步I/O操作也会存在。这就像终结器线程一样。你无法真正说有一个“浪费”的线程被用于那个异步操作,就像你不能说有一个“浪费”的线程用于等待finalizable对象进行垃圾回收一样。有一个终结器线程,但它是共享的,就像IOCP一样。更多信息在这里 - Stephen Cleary
1
谢谢。我感兴趣的是:在异步WebAPI操作方法出现之前,如何处理请求而不需要等待线程? - cmart
1
@MarChr:ASP.NET很早就支持异步模块和处理程序了。通常它们使用APM(异步编程模型) - Stephen Cleary

4
异步编程的目的并不是使应用程序多线程,而是让单线程应用程序在等待来自正在执行于不同线程或进程的外部调用的响应时,能够继续处理其他事情。
考虑一个桌面应用程序,它显示来自不同交易所的股票价格。该应用程序需要进行一些 REST/http 调用以从每个远程股票交易所服务器获取一些数据。
单线程应用程序会发出第一个调用,等待直到获得第一组价格,更新其窗口,然后发出对下一个外部股票价格服务器的调用,再次等待直到获得价格,更新其窗口...等等。
我们可以将所有请求并行启动并在并行中更新屏幕,但由于大部分时间都花在等待远程服务器的响应上,这似乎有些过度。
线程最好是: 为第一个服务器发出请求,但不要等待答案,而是留下标记,即当价格到达时返回的位置,并继续发出第二个请求,再次留下标记...等等。
当所有请求都已发出时,应用程序执行线程可以继续处理用户输入或所需内容。
现在,当接收到来自其中一个服务器的响应时,可以将线程定向到先前放置的标记处并更新窗口。
所有这些都可以手动编码,单线程方式,但是这样做非常麻烦,多线程通常更容易。现在,在编写 async/await 时编译器会处理留下标记并返回的过程。全部都是单线程。
这里有两个关键点:
1)多线程仍然会发生!我们对股票价格的请求处理发生在不同的线程上(在不同的机器上)。如果我们进行数据库访问,则情况也是如此。在等待计时器的示例中,计时器在不同的线程上运行。我们的应用程序是单线程的,执行点只是跳来跳去(以受控制的方式),而外部线程则在执行。
2)一旦应用程序需要异步操作完成,我们就失去了异步执行的好处。考虑一个显示两个交易所咖啡价格的应用程序,该应用程序可以在单个线程上异步启动请求并更新其窗口,但是现在,如果该应用程序还计算两个交易所之间的价格差异,则必须等待异步调用完成。这是强制性的,因为异步方法(例如,我们可能编写的用于调用股票价格的交易所的异步方法)不会返回股票价格,而是返回任务(Task),可以将其视为返回放置标记的方法,以便函数可以完成并返回股票价格。
这意味着每个调用异步函数的函数都需要是异步的或等待最底部的调用堆栈中的“其他线程/进程/机器”调用完成,如果我们在等待底部调用完成,那么为什么还要使用异步呢?

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


4
异步操作的好处在于,当控制器等待 SQL 查询完成时,没有线程为此请求分配资源。如果您使用同步方法,一个线程将从开始到结束一直被锁定在该方法的执行中。在 SQL Server 执行其工作时,该线程不会做任何事情,只是等待。 如果使用异步方法,该线程可以在 SQL Server 工作时响应其他请求。
我认为您在第四步的步骤有误,我认为它不会创建一个新的线程来执行 SQL 查询。在第六步,没有创建一个新的线程,只是其中一个可用的线程将被用于继续从第一个线程离开的地方继续执行。第六个线程可能是启动异步操作的同一个线程。

那么,我可以编写一个应用程序,可以启动两个同时的SQL查询,打印它们的结果,并且不会在任何时候使用超过一个线程吗? 我想看到这个答案的概念证明。链接或代码。 - Rustem Mustafin

3
在我看来,异步控制器比同步控制器的一个明显优点在于以下内容。当使用同步方法处理高延迟调用的Web应用程序,线程池增长到.NET 4.5默认的最大线程数5000时,与能够使用异步方法并且只有50个线程服务相同请求的应用程序相比,会消耗约5GB更多的内存。在进行异步工作时,并非总是在使用线程。例如,在进行异步Web服务请求时,ASP.NET 在异步方法调用和 await 之间不会使用任何线程。使用线程池来处理具有高延迟的请求可能导致较大的内存占用和服务器硬件的利用率不佳。 来源:在 ASP.NET MVC 4 中使用异步方法

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接