ASP.NET控制器:在异步操作仍处于挂起状态时完成的异步模块或处理程序

dya*_*nko 43 .net c# webclient async-await asp.net-mvc-4

我有一个非常简单的ASP.NET MVC 4控制器:

public class HomeController : Controller
{
    private const string MY_URL = "http://smthing";
    private readonly Task<string> task;

    public HomeController() { task = DownloadAsync(); }

    public ActionResult Index() { return View(); }

    private async Task<string> DownloadAsync()
    {
        using (WebClient myWebClient = new WebClient())
            return await myWebClient.DownloadStringTaskAsync(MY_URL)
                                    .ConfigureAwait(false);
    }
}
Run Code Online (Sandbox Code Playgroud)

当我启动项目时,我看到了我的视图,它看起来很好,但是当我更新页面时,我收到以下错误:

[InvalidOperationException:在异步操作仍处于挂起状态时完成异步模块或处理程序.

为什么会这样?我做了几个测试:

  1. 如果我们task = DownloadAsync();从构造函数中删除并将其放入Index方法中它将正常工作而没有错误.
  2. 如果我们使用另一个DownloadAsync()身体return await Task.Factory.StartNew(() => { Thread.Sleep(3000); return "Give me an error"; });它将正常工作.

为什么不可能WebClient.DownloadStringTaskAsync在控制器的构造函数中使用该方法?

Yuv*_*kov 54

Async Void,ASP.Net和Outstanding Operations中,Stephan Cleary解释了这个错误的根源:

从历史上看,ASP.NET通过基于事件的异步模式(EAP)支持自.NET 2.0以来的干净异步操作,其中异步组件通知SynchronizationContext它们的启动和完成.

发生的事情是你DownloadAsync在你的类构造函数中触发,在你await的异步http调用里面.这将注册与ASP.NET的异步操作SynchronizationContext.当你HomeController返回时,它会看到它有一个尚未完成的挂起异步操作,这就是它引发异常的原因.

如果我们删除task = DownloadAsync(); 从构造函数并将其放入Index方法,它将正常工作,没有错误.

正如我上面解释的那样,那是因为从控制器返回时不再有挂起的异步操作.

如果我们使用另一个DownloadAsync()体返回等待 Task.Factory.StartNew(() => { Thread.Sleep(3000); return "Give me an error"; });它将正常工作.

那是因为Task.Factory.StartNew在ASP.NET中做了一些危险的事情.它不会使用ASP.NET注册任务执行.这可能导致执行池循环的边缘情况,完全忽略后台任务,导致异常中止.这就是为什么你必须使用一个注册任务的机制,例如HostingEnvironment.QueueBackgroundWorkItem.

这就是为什么不可能做你正在做的事情,就像你做的那样.如果你真的希望在后台线程中以"即发即忘"的方式执行它,请使用HostingEnvironment(如果你使用的是.NET 4.5.2)或BackgroundTaskManager.请注意,通过执行此操作,您将使用线程池线程执行异步IO操作,这是多余的,并且正是async-await尝试克服的异步IO .

  • 感谢您的回答.我已经阅读了你提到的文章,但我又读了一遍.我不知道为什么我之前没有意识到这一点,但我的问题的关键应该是第二个短语:"MVC框架理解如何等待你的'任务',但它甚至不知道`async void`,所以它返回ASP.NET核心的完成,它看到它实际上并不完整." 控制器构造函数被编译成`void`方法,这就是我收到错误的原因.我对吗? (2认同)

Ste*_*ner 7

我今天在构建 API 控制器时遇到了这个错误。事实证明,对于我来说,解决方案很简单。

我有:

public async void Post()
Run Code Online (Sandbox Code Playgroud)

我需要将其更改为:

public async Task Post()
Run Code Online (Sandbox Code Playgroud)

请注意,编译器没有警告async void.


bin*_*nki 6

ASP.NET 认为启动与其绑定的“异步操作”SynchronizationContextActionResult在所有启动的操作完成之前返回一个是非法的。所有async方法都将自己注册为“异步操作”,因此您必须确保绑定到 ASP.NET 的所有此类调用SynchronizationContext在返回ActionResult.

在您的代码中,您在没有确保DownloadAsync()已运行完成的情况下返回。但是,您将结果保存到task成员中,因此确保这是完整的非常容易。只需await task在返回之前放入所有操作方法(在异步化它们之后):

public async Task<ActionResult> IndexAsync()
{
    try
    {
        return View();
    }
    finally
    {
        await task;
    }
}
Run Code Online (Sandbox Code Playgroud)

编辑:

在某些情况下,您可能需要调用一个在返回 ASP.NET 之前不应完成async方法。例如,您可能希望延迟初始化一个后台服务任务,该任务应在当前请求完成后继续运行。OP 的代码不是这种情况,因为 OP 希望任务在返回之前完成。但是,如果您确实需要开始而不是等待任务,则有一种方法可以做到这一点。您只需要使用一种技术来“逃避”当前的.SynchronizationContext.Current

  • 不推荐) 的一个特性Task.Run()是转义当前的同步上下文。但是,人们建议不要在 ASP.NET 中使用它,因为 ASP.NET 的线程池是特殊的。此外,即使在 ASP.NET 之外,这种方法也会导致额外的上下文切换。

  • 推荐)在不强制额外的上下文切换或立即打扰 ASP.NET 的线程池的情况下转义当前同步上下文的安全方法是设置SynchronizationContext.Currentnull,调用您的async方法,然后恢复原始值