深入理解 ASP.NET MVC 上的 async/await

Posted

技术标签:

【中文标题】深入理解 ASP.NET MVC 上的 async/await【英文标题】:Deep understanding of async / await on ASP.NET MVC 【发布时间】:2015-12-10 22:47:26 【问题描述】:

当我在 MVC 控制器上执行异步操作时,尤其是在处理 I/O 操作时,我不明白幕后究竟发生了什么。假设我有一个上传操作:

public async Task<ActionResult> Upload (HttpPostedFileBase file) 
  ....
  await ReadFile(file);

  ...

据我所知,这些是发生的基本步骤:

    从线程池中窥视一个新线程并分配给处理传入请求。

    当 await 被命中时,如果调用是 I/O 操作,那么原始线程将返回到池中,并将控制权转移到所谓的 IOCP(输入输出完成端口)。我不明白为什么请求仍然存在并等待答案,因为最终调用客户端将等待我们的请求完成。

我的问题是:谁/何时/如何等待完全阻塞发生?

注意:我看到了博文 There Is No Thread,它对 GUI 应用程序很有意义,但对于这种服务器端场景,我不明白。真的。

【问题讨论】:

您到底在寻找什么:解释常规 IHttpAsyncHandler 如何与 async/await 集成或为什么 IHttpAsyncHandler 完全有效或其他什么? 我很难理解哪一部分你不明白,你能澄清一下吗?如果将处理线程放回池中,则请求没有理由终止 - 它稍后会恢复,可能在另一个线程上。 BTW 控制未转移到 IOCP。 考虑看这篇文章,它解释了 MVC 中的异步等待 qawithexperts.com/article/c-sharp/… 【参考方案1】:

网上有一些很好的资源可以详细描述这一点。我写了一个MSDN article that describes this at a high level。

我不明白为什么请求仍然存在并等待答案,因为最终调用客户端将等待我们的请求完成。

它仍然存在,因为 ASP.NET 运行时尚未完成它。完成请求(通过发送响应)是一个明确的动作;这不像请求会自行完成。当 ASP.NET 看到控制器操作返回 Task/Task&lt;T&gt; 时,它不会在该任务完成之前完成请求。

我的问题是:谁/何时/如何等待完全阻塞发生?

没有什么在等待。

这样想:ASP.NET 有一个它正在处理的当前请求的集合。对于给定的请求,一旦完成,就会发出响应,然后从集合中删除该请求。

关键是它是请求的集合,而不是线程。这些请求中的每一个在任何时间点都可能有也可能没有线程在处理它。同步请求总是有一个线程(同一个线程)。异步请求可能有一段时间没有线程。

注意:我看到了这个帖子:http://blog.stephencleary.com/2013/11/there-is-no-thread.html,它对 GUI 应用程序有意义,但对于这个服务器端场景我不明白。

无线程 I/O 方法对 ASP.NET 应用程序的工作方式与它对 GUI 应用程序的工作方式完全相同。

最终,文件写入将完成,这(最终)完成了从ReadFile 返回的任务。这种“完成任务”的工作通常是用线程池线程来完成的。由于任务现已完成,Upload 操作将继续执行,导致该线程进入请求上下文(即,现在有一个线程再次执行该请求)。当Upload 方法完成时,则从Upload 返回的任务完成,ASP.NET 写出响应并将请求从其集合中删除。

【讨论】:

"关键是它是请求的集合,而不是线程。这些请求中的每一个在任何时候都可能有也可能没有线程在处理它。同步请求总是有一个线程(同一个线程)。异步请求可能有一段时间没有线程。”很好地清除它。【参考方案2】:

在后台,编译器会耍花招,将您的 async \ await 代码转换为带有回调的基于 Task 的代码。在最简单的情况下:

public async Task X()

    A();
    await B();
    C();

变成类似这样的东西:

public Task X()

    A();
    return B().ContinueWith(()=> C(); )

所以没有魔法 - 只是很多 Tasks 和回调。对于更复杂的代码,转换也会更复杂,但最终生成的代码将在逻辑上等同于您编写的代码。如果需要,您可以使用 ILSpy/Reflector/JustDecompile 之一,亲自查看“幕后”编译的内容。

反过来,ASP.NET MVC 基础结构足够智能,可以识别您的操作方法是普通方法还是基于Task 的方法,并依次改变其行为。因此请求不会“消失”。

一个常见的误解是async 的所有内容都会产生另一个线程。事实上,它大多是相反的。在async Task 方法长链的末端通常有一个方法执行一些异步 IO 操作(例如从磁盘读取或通过网络通信),这是 Windows 本身执行的神奇操作。在此操作期间,根本没有与代码关联的线程 - 它实际上已停止。然而,在操作完成后,Windows 会回调,然后分配线程池中的一个线程以继续执行。需要一些框架代码来保留请求的HttpContext,但仅此而已。

【讨论】:

【参考方案3】:

ASP.NET 运行时了解什么是任务,并延迟发送 HTTP 响应,直到任务完成。实际上,需要 Task.Result 值才能生成响应。

运行时基本上是这样做的:

var t = Upload(...);
t.ContinueWith(_ => SendResponse(t));

因此,当您的await 被击中时,您的代码和运行时代码都会从堆栈中退出,并且此时“没有线程”。 ContinueWith 回调恢复请求并发送响应。

【讨论】:

以上是关于深入理解 ASP.NET MVC 上的 async/await的主要内容,如果未能解决你的问题,请参考以下文章

使 ASP.net MVC 5 Routing Ignore Async Suffix on Action 方法

深入研究 Mini ASP.NET Core(迷你 ASP.NET Core),看看 ASP.NET Core 内部到底是如何运行的

为啥总是在 asp.net mvc 中同步异步操作(async await)

asp.net mvc中在使用async的时候HttpContext为null的问题

简述关于ASP.NET MVC与.NET CORE 的区别

七天学会ASP.NET MVC ——线程问题异常处理自定义URL