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

时间:2015-03-02 08:56:33

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

我有一个非常简单的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"; });,它将正常运作。
  3. 为什么不可能在控制器的构造函数中使用WebClient.DownloadStringTaskAsync方法?

8 个答案:

答案 0 :(得分:47)

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

  

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

正在发生的事情是您在类构造函数中触发DownloadAsync,在异步http调用中await内部SynchronizationContext。这将注册异步操作与ASP.NET HomeController。当你的Task.Factory.StartNew(() => { Thread.Sleep(3000); return "Give me an error"; });返回时,它会看到它有一个尚未完成的挂起异步操作,这就是它引发异常的原因。

  

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

正如我上面所解释的那样,因为从控制器返回时你不再有待处理的异步操作。

  

如果我们使用另一个DownloadAsync()体返回await   Task.Factory.StartNew它会正常运作。

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

这就是为什么不能做你正在做的事情,以及你做这件事的方式。如果你真的希望这个在后台线程中执行,那就是“即兴完成”。样式,使用async-await(如果您使用的是.NET 4.5.2)或BackgroundTaskManager。请注意,通过执行此操作,您将使用线程池线程执行异步IO操作,这是多余的,并且正是{{1}}尝试克服的异步IO。

答案 1 :(得分:3)

ASP.NET认为启动绑定到其 $i_header["header"] = "PayPal-Mock-Response"; $i_header["value"] = json_encode(["mock_application_codes" => "INSTRUMENT_DECLINED"]); $o_apiContext->addRequestHeader($i_header["header"], $i_header["value"]); 的“异步操作”并在所有已启动的操作完成之前返回SynchronizationContext是非法的。所有ActionResult方法都将自己注册为“异步操作”,因此您必须确保在返回async之前完成绑定到ASP.NET SynchronizationContext的所有此类调用。

在您的代码中,您无需确保ActionResult已完成运行即可返回。但是,您将结果保存到DownloadAsync()成员,因此确保完成此操作非常简单。在返回之前,只需将task放入所有操作方法(在异步后):

await task

编辑:

在某些情况下,您可能需要调用{strong}在返回ASP.NET之前不应完成的public async Task<ActionResult> IndexAsync() { try { return View(); } finally { await task; } } 方法。例如,您可能希望懒惰地初始化后台服务任务,该任务应在当前请求完成后继续运行。 OP的代码不是这种情况,因为OP希望在返回之前完成任务。但是,如果您确实需要启动而不是等待任务,则有一种方法可以执行此操作。您只需使用一种技术从当前async“逃避”。

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

  • 推荐)转义当前同步上下文而不强制进行额外的上下文切换或立即打扰ASP.NET的线程池的一种安全方法是set SynchronizationContext.Current to null, call your async method, and then restore the original value

答案 2 :(得分:2)

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

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

答案 3 :(得分:1)

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

答案 4 :(得分:1)

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

答案 5 :(得分:0)

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

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

答案 6 :(得分:0)

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

答案 7 :(得分:0)

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

我有:

public async void Post()

我需要将其更改为:

public async Task Post()

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