MVC4 + async/await + 在操作完成前返回响应

16
在我的MVC4应用程序中,我需要添加一个控制器来上传和处理大文件。在文件上传后,我需要立即开始异步处理该文件,并在不等待处理完成的情况下向浏览器返回响应。
显然,我可以手动启动一个新线程来处理文件,但我想知道是否可以使用.net 4.5引入的异步/等待机制来实现此场景。
为了测试这个概念,我尝试了类似于以下内容的代码:
public async Task<ActionResult> Test()
{
    TestAsync();
    return View("Test");
}

public async void TestAsync()
{
    await LongRunning();
}

private Task<int> LongRunning()
{
    return Task<int>.Factory.StartNew(() => Pause());
}

private int Pause()
{
    Thread.Sleep(10000);
    return 3;
}

在一般情况下,异步机制似乎可以正常工作:当我调试代码时,在"return 3"这一行之前我会命中"return View("Test");" 。然而,浏览器只有在Pause方法完成后才接收到响应。

这似乎像是常规的异步控制器(具有Async和Completed方法)。是否有一种方法能够在我的情况下在控制器中使用异步/等待?


1
我不确定在ASP.NET应用程序中直接执行任何长时间处理是一个好主意,因为IIS可以在任何时候回收您的应用程序。 - svick
我对新的异步操作不太熟悉,但是LongRunning应该在自己的线程中执行吧?Thread.Sleep会让当前线程休眠,也就是执行该操作的线程。 - Matt Greer
我必须在这里同意svick的观点。如果您有长时间运行的异步请求,应该在其他地方处理它们。通常使用持久队列(如MSMQ、Azure、RabbitMQ等)和其他东西(如Windows服务、由任务计划程序运行的exe、使用Quartz.net的应用程序等)来处理它们。持久队列还意味着,如果您必须在Web应用程序中进行处理,至少可以在重新启动时不会丢失数据(尽管通常会丢失正在处理中的中间结果)。 - James Manning
3个回答

14
显然,我可以为手动处理文件启动一个新线程,但我想知道是否可以使用引入的 .net 4.5 的 async/await 机制来实现这种情况。不,你不能,因为async 不会改变 HTTP 协议。Svick 和 James 已经在评论中发布了正确的答案,以下是我为了方便重复的内容:IIS 可以在任何时候回收您的应用程序。如果您需要异步处理请求中的长时间运行的事务,请将它们放在其他地方。通常的做法是使用持久队列(MSMQ、Azure、RabbitMQ 等)和其他一些东西(Windows 服务、由任务计划程序运行的 exe、使用 Quartz.net 的应用程序等)进行处理。综上所述,HTTP 仅提供一个请求和一个响应(async - 以及其他任何东西 - 都不会改变这一点)。
ASP.NET是围绕HTTP请求设计的(并且做出了假设,比如“如果没有未完成的请求,则可以安全地停止此网站”)。您可以启动一个新线程并将上传保留在内存中(这是最简单的方法),但强烈不建议这样做。
对于您的情况,我建议您遵循James的建议:
- 当上传完成时,将上传保存到持久存储(例如Azure队列)中,并向浏览器返回“票证”。 - 有一些其他的进程(例如Azure Worker Role)处理队列,最终将其标记为完成。 - 让浏览器使用其“票证”轮询服务器。服务器使用“票证”查找持久存储中的文件并返回其是否已完成。
这有一些变化(例如使用SignalR通知浏览器处理完成),但总体架构是相同的。
这很复杂,但这是正确的做法。

1
Stephen,感谢您的回复。只是想澄清,在以下场景中:
  1. 我手动启动一个新线程来处理文件并将其进度写入会话对象。
  2. 在该线程完成之前,我将响应返回给浏览器。
  3. 使用AJAX调用来检查/显示进度。
这个文件处理线程是否有可能在完成之前被回收?
- filip
是的。整个过程将被循环利用。 - Stephen Cleary
有没有一些好的资源可以指导如何做到这一点?除了Azure,还有其他选择吗? - Paul Smith
我想不出任何专门描述这个的资源。如果您有自己的Web服务器,可以使用MSMQ进行持久性存储,而不是Azure队列和Win32服务,而不是Azure工作角色。 - Stephen Cleary

2

您的代码中存在错误。 您必须在操作中等待对TestAsync的调用。 否则,它仅返回一个“Task”对象而不运行任何内容。

public async Task<ActionResult> Test()
{
    await TestAsync();
    return View("Test");
}

在异步方法中正确的睡眠方式是调用。
await Task.Delay(10000);

你需要更改TestAsync的签名:如果方法返回void,则不应将其标记为async。它必须返回Task。返回void仅用于与.net事件兼容。

public async Task TestAsync()
{
    await LongRunning();
}

方法体是相同的。

你不能 await TestAsync(),因为它是返回 void 的。 - svick
1
如果函数被标记为async,就不应该返回void。这是为了与事件兼容而保留的。在这种情况下,只需返回Task而不是void。 - Softlion

0

你的LongRunning方法正在同步睡眠10秒。将其更改为在任务中发生睡眠。


詹姆斯,我已經更新了我的示例代碼,包括你的建議。但問題仍然存在,即瀏覽器只有在Pause()方法完成後才收到響應。 - filip
@filip - 如果在TestAsync中使用ConfigureAwait(false),行为会有什么变化吗?也许它不会允许完成,直到所有使用该同步上下文的挂起任务都完成。在这种情况下,您可能不需要使用await,只需启动一个普通任务并返回即可。 - James Manning

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