【问题标题】:ASP.NET Controller: An asynchronous module or handler completed while an asynchronous operation was still pendingASP.NET 控制器:异步模块或处理程序已完成,而异步操作仍处于挂起状态
【发布时间】:2023-03-11 21:28:01
【问题描述】:

我有一个非常简单的 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);
    }
}

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

[InvalidOperationException:异步模块或处理程序已完成,而异步操作仍处于挂起状态。]

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

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

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

【问题讨论】:

    标签: c# .net asp.net-mvc-4 async-await webclient


    【解决方案1】:

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

    从历史上看,ASP.NET 一直支持干净的异步操作 从 .NET 2.0 开始,通过基于事件的异步模式 (EAP),在 哪些异步组件通知 SynchronizationContext 他们的开始和结束。

    正在发生的事情是您在类构造函数中触发了DownloadAsync,而在异步http调用中您的内部await。这将向 ASP.NET SynchronizationContext 注册异步操作。当您的HomeController 返回时,它会看到它有一个尚未完成的待处理异步操作,这就是它引发异常的原因。

    如果我们删除任务 = 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 框架知道如何等待你的Task,但它甚至不知道async void,因此它将完成返回给 ASP.NET 核心,它发现它实际上并不完整。”控制器构造函数被编译到 void 方法中,这就是我收到错误的原因。我说的对吗?
    • @dyatchenko 不。这与async void 无关。 ASP.NET 知道如何处理async Task。因为它注册到SynchronizationContext,所以当你的控制器返回时,它会注意到你的DownloadAsync 方法没有完成。
    • 好的。在这种情况下,正如您所提到的,我可以等待几秒钟,而我的下载完成,然后我可以刷新页面,它应该没问题,但事实并非如此。
    • 我什至不确定这意味着什么。您可以同步执行此代码,也可以使用我在答案中提到的适当措施在线程池线程上执行它。但是你不能像现在这样异步执行它。
    • “那是因为你在从控制器返回时不再有一个挂起的异步操作”——你能简要说明一下为什么会这样吗?当Index() 被内部task = DownloadAsync(); 调用时,在我看来,当Index() 完成时,异步操作也会继续进行。
    【解决方案2】:

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

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

    public async Task<ActionResult> IndexAsync()
    {
        try
        {
            return View();
        }
        finally
        {
            await task;
        }
    }
    

    编辑:

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

    【讨论】:

      【解决方案3】:

      我遇到了相关问题。客户端正在使用一个返回 Task 的接口,并使用异步实现。

      在 Visual Studio 2015 中,异步且在调用方法时不使用 await 关键字的客户端方法不会收到警告或错误,代码编译干净。竞争条件被提升为生产环境。

      【讨论】:

      • 感谢您的回答。我遇到了完全相同的问题,但这是导致 OP 描述的相同问题的不同方式。
      【解决方案4】:

      方法返回async TaskConfigureAwait(false)可以是解决方案之一。它将像 async void 一样运行,并且不会继续同步上下文(只要您真的不关心方法的最终结果)

      【讨论】:

        【解决方案5】:

        方法 myWebClient.DownloadStringTaskAsync 在单独的线程上运行并且是非阻塞的。一种可能的解决方案是使用 myWebClient 的 DownloadDataCompleted 事件处理程序和 SemaphoreSlim 类字段来执行此操作。

        private SemaphoreSlim signalDownloadComplete = new SemaphoreSlim(0, 1);
        private bool isDownloading = false;
        

        ....

        //Add to DownloadAsync() method
        myWebClient.DownloadDataCompleted += (s, e) => {
         isDownloading = false;
         signalDownloadComplete.Release();
        }
        isDownloading = true;
        

        ...

        //Add to block main calling method from returning until download is completed 
        if (isDownloading)
        {
           await signalDownloadComplete.WaitAsync();
        }
        

        【讨论】:

          【解决方案6】:

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

          我有:

          public async void Post()
          

          我需要将其更改为:

          public async Task Post()
          

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

          【讨论】:

            【解决方案7】:

            我遇到了类似的问题,但通过将 CancellationToken 作为参数传递给异步方法得到了解决。

            【讨论】:

              【解决方案8】:

              带附件的电子邮件通知示例..

              public async Task SendNotification(string SendTo,string[] cc,string subject,string body,string path)
                  {             
                      SmtpClient client = new SmtpClient();
                      MailMessage message = new MailMessage();
                      message.To.Add(new MailAddress(SendTo));
                      foreach (string ccmail in cc)
                          {
                              message.CC.Add(new MailAddress(ccmail));
                          }
                      message.Subject = subject;
                      message.Body =body;
                      message.Attachments.Add(new Attachment(path));
                      //message.Attachments.Add(a);
                      try {
                           message.Priority = MailPriority.High;
                          message.IsBodyHtml = true;
                          await Task.Yield();
                          client.Send(message);
                      }
                      catch(Exception ex)
                      {
                          ex.ToString();
                      }
               }
              

              【讨论】:

              • 这与问题有何关系?你应该在那里使用client.SendMailAsync(message)。为什么要等待任务产量?
              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2015-07-30
              • 2013-03-08
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多