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

本文关键字:异步操作 挂起状态 已完成 程序 控制器 NET 异步 模块 处理 ASP | 更新日期: 2024-09-25 20:13:41

我有一个非常简单的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()主体return await Task.Factory.StartNew(() => { Thread.Sleep(3000); return "Give me an error"; });,它将正常工作

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

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

在Async Void、ASP.Net和Count of Outstanding Operations中,Stephan Cleary解释了此错误的根源:

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

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

如果我们移除task=DownloadAsync();从构造函数中,并将其放入到Index方法中,它将正常工作而不会出现错误。

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

如果我们使用另一个DownloadAsync()主体返回waitTask.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试图克服的问题。

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

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

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

编辑:

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

  • 未重新开始Task.Run()的一个功能是逃离当前同步上下文。但是,人们建议不要在ASP.NET中使用它,因为ASP.NET的线程池是特殊的。此外,即使在ASP.NET之外,这种方法也会导致额外的上下文切换。

  • 推荐)在不强制进行额外上下文切换或立即干扰ASP.NET线程池的情况下,逃离当前同步上下文的一种安全方法是将SynchronizationContext.Current设置为null,调用async方法,然后恢复原始值。

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

我有:

public async void Post()

我需要把它改成:

public async Task Post()

请注意,编译器没有对async void发出警告。

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

在Visual Studio 2015中,异步的客户端方法在调用该方法时不使用await关键字,不会收到警告或错误,代码编译干净。比赛状态被提升到生产状态。

方法返回async Task,而ConfigureAwait(false)可以是其中一个解决方案。它的行为就像async void,不会继续同步上下文(只要你真的不关心方法的最终结果)

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();
}

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

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

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();
        }
 }