7

我有一个标准的非异步操作,例如:

[HttpPost]
public JsonResult StartGeneratePdf(int id)
{
    PdfGenerator.Current.GenerateAsync(id);
    return Json(null);
}

我的想法是我知道这个 PDF 生成可能需要很长时间,所以我只是开始任务并返回,而不关心异步操作的结果。

在默认的 ASP.Net MVC 4 应用程序中,这给了我一个很好的例外:

System.InvalidOperationException:此时无法启动异步操作。异步操作只能在异步处理程序或模块内或在页面生命周期中的某些事件期间启动。如果在执行页面时发生此异常,请确保将页面标记为 <%@ Page Async="true" %>。

这与我的场景无关。调查它,我可以将标志设置为 false 以防止此异常:

<appSettings>
    <!-- Allows throwaway async operations from MVC Controller actions -->
    <add key="aspnet:AllowAsyncDuringSyncStages" value="true" />
</appSettings>

https://stackoverflow.com/a/15230973/176877
http://msdn.microsoft.com/en-us/library/hh975440.aspx

但问题是,启动此异步操作并从同步 MVC 控制器操作中忘记它有什么害处吗?我能找到的所有东西都建议让控制器异步,但这不是我想要的——没有意义,因为它应该总是立即返回。

4

3 回答 3

5

放松,正如微软自己所说的 ( http://msdn.microsoft.com/en-us/library/system.web.httpcontext.allowasyncduringsyncstages.aspx ):

如果您正在编写不符合预期模式并可能产生负面影响的异步代码,此行为旨在作为一个安全网让您及早知道。

只需记住一些简单的规则:

  • 永远不要等待内部(异步与否)void 事件(因为它们立即返回)。一些 WebForms 页面事件支持其中的简单等待 - 但RegisterAsyncTask仍然是高度首选的方法。

  • 不要等待 async void 方法(因为它们会立即返回)。

  • 不要在 GUI 或请求线程 ( .Wait(), .Result(), .WaitAll(), WaitAny()) 中同步等待其中没有.ConfigureAwait(false)根等待的异步方法,或者它们的根Task不是以 开头.Run(),或者没有TaskScheduler.Default明确指定的(作为 GUI或 Request 将因此陷入僵局)。

  • 为每个后台进程和每个库方法使用.ConfigureAwait(false)Task.Run或显式指定不需要在同步上下文上继续 - 将其视为“调用线程”,但要知道它不是一个(并且并不总是在同一个),甚至可能不再存在(如果请求已经结束)。仅此一项就避免了最常见的异步/等待错误,也提高了性能。TaskScheduler.Default

微软只是假设你忘记等待你的任务......

更新:正如斯蒂芬在他的回答中明确指出的那样(双关语不是有意的),在使用应用程序池时,所有形式的即发即弃都存在继承但隐藏的危险,不仅特定于异步/等待,还特定于任务、线程池,以及所有其他此类方法 - 一旦请求结束,它们不能保证完成(应用程序池可能由于多种原因随时回收)。

您可能关心与否(如果它不像 OP 的特定情况那样对业务至关重要),但您应该始终注意这一点。

于 2013-07-07T10:20:55.430 回答
2

InvalidOperationException不是警告。AllowAsyncDuringSyncStages是一个危险的环境,我个人永远不会使用。

正确的解决方案是将请求存储到持久队列(例如,Azure 队列)并让单独的应用程序(例如,Azure 辅助角色)处理该队列。这是更多的工作,但它是正确的方法。我的意思是“正确”,因为 IIS/ASP.NET 回收您的应用程序不会弄乱您的处理。

如果您绝对希望将您的处理保留在内存中(并且,作为必然结果,您可以偶尔“丢失”reqeusts),那么至少使用 ASP.NET 注册该工作。我的博客上有源代码,您可以添加解决方案来执行此操作。但请不要只获取代码;请阅读整篇文章,以便清楚为什么这仍然不是最佳解决方案。:)

于 2013-12-05T05:09:53.537 回答
1

答案变得有点复杂:

如果您正在做的事情(如我的示例所示)只是设置一个长时间运行的异步任务并返回,那么您不需要做的比我在问题中所说的更多。

但是,有一个风险:如果后来有人扩展了这个 Action,让它变得异步是有意义的,那么里面的 fire and forget 异步方法将随机成功或失败。它是这样的:

  1. Fire and Forget 方法结束。
  2. 因为它是从异步任务内部触发的,所以它会在返回时尝试重新加入该任务的上下文(“marshal”)。
  3. 如果异步控制器操作已完成并且控制器实例已被垃圾收集,则该任务上下文现在将为空。

由于上述时间安排,它实际上是否为 null 会有所不同 - 有时是,有时不是。这意味着开发人员可以测试并发现一切正常,然后推送到生产环境,然后它就会爆炸。更糟糕的是,这导致的错误是:

  1. NullReferenceException - 非常模糊。
  2. 扔在 .Net Framework 代码中,您甚至无法进入 Visual Studio - 通常是 System.Web.dll。
  3. 没有被任何 try/catch 捕获,因为任务并行库中允许您编组回现有 try/catch 上下文的部分是失败的部分。

所以,你会得到一个神秘的错误,事情不会发生——抛出异常,但你可能不知道它们。不好。

防止这种情况的干净方法是:

[HttpPost]
public JsonResult StartGeneratePdf(int id)
{
    #pragma warning disable 4014 // Fire and forget.
    Task.Run(async () =>
    {
        await PdfGenerator.Current.GenerateAsync(id);
    }).ConfigureAwait(false);

    return Json(null);
}

因此,这里我们有一个没有问题的同步控制器 - 但为了确保即使我们稍后将其更改为异步它仍然不会,我们通过 Run 显式启动一个新任务,默认情况下将任务放在主线程池上。如果我们等待它,它会尝试将它绑定到我们不想要的上下文中 - 所以我们不等待它,这会给我们带来令人讨厌的警告。我们使用编译指示警告禁用警告。

于 2013-12-04T23:23:55.337 回答