ASP.NET 控制器:异步模块或处理程序已完成,而异步操作仍未决

ASP.NET Controller: An asynchronous module or handler completed while an asynchronous operation was still pending

我有一个非常简单的 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: An asynchronous module or handler completed while an asynchronous operation was still pending.]

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

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

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

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

Historically, ASP.NET has supported clean asynchronous operations since .NET 2.0 via the Event-based Asynchronous Pattern (EAP), in which asynchronous components notify the SynchronizationContext of their starting and completing.

发生的事情是,您在 class 构造函数中触发 DownloadAsync,而在您内部 await 异步 http 调用中。这将使用 ASP.NET SynchronizationContext 注册异步操作。当您的 HomeController returns 时,它发现它有一个尚未完成的挂起异步操作,这就是它引发异常的原因。

If we remove task = DownloadAsync(); from the constructor and put it into the Index method it will work fine without the errors.

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

If we use another DownloadAsync() body return await Task.Factory.StartNew(() => { Thread.Sleep(3000); return "Give me an error"; }); it will work properly.

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

这就是为什么不可能按照您的方式做您正在做的事情。如果您确实希望它以 "fire-and-forget" 样式在后台线程中执行,请使用 HostingEnvironment(如果您使用的是 .NET 4.5.2)或 BackgroundTaskManager。请注意,通过这样做,您正在使用线程池线程来执行异步 IO 操作,这是多余的,并且正是 async-await 试图克服的异步 IO。

我运行进入相关问题。客户端正在使用 returns 任务的接口并使用异步实现。

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

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

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

方法returnasync TaskConfigureAwait(false)可以是其中一种解决方法。它会像 async void 一样,不会继续同步上下文(只要你真的不关心方法的最终结果)

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

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

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

在您的代码中,您 return 没有确保 DownloadAsync() 具有 运行 完成。但是,您将结果保存到 task 成员,因此很容易确保这是完整的。只需在 returning:

之前将 await task 放入所有操作方法中(在将它们异步化之后)
public async Task<ActionResult> IndexAsync()
{
    try
    {
        return View();
    }
    finally
    {
        await task;
    }
}

编辑:

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

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

  • (recommended) 一种安全的方式来逃避当前的同步上下文,而不强制额外的上下文切换或立即打扰 ASP.NET 的线程池至 set SynchronizationContext.Current to null, call your async method, and then restore the original value.

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

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

我有:

public async void Post()

我需要将其更改为:

public async Task Post()

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