2015-03-02 67 views
25

我有一个非常简单的ASP.NET MVC 4控制器的异步模块或处理程序:ASP.NET控制器:完成,而异步操作仍然悬而未决

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方法?

回答

33

Async Void, ASP.Net, and Count of Outstanding Operations,斯蒂芬·克利里解释了这个错误的根源:

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.

正在发生的事情是,你射击DownloadAsync类构造函数里面,在那里你内心await对异步HTTP调用。这注册了与ASP.NET SynchronizationContext的异步操作。当您的HomeController返回时,它会看到它有一个尚未完成的待处理异步操作,这就是它引发异常的原因。

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

这就是为什么你不可能做你正在做的事情,你这样做的方式。如果您确实希望在后台线程中以“即发即弃”的风格执行此操作,请使用HostingEnvironment(如果您使用的是.NET 4.5.2)或BackgroundTaskManager。请注意,通过这样做,您正在使用线程池线程来执行异步IO操作,这是多余的,并且与async-await尝试克服的异步IO完全相同。

+2

感谢您的回答。我已经阅读过你提到的那篇文章,但我再读一遍。我不知道为什么我以前没有意识到它,但我的问题的关键应该是第二句话:“MVC框架了解如何等待您的”任务“,但它甚至不知道'async void',所以它向ASP.NET内核返回完成,它看到它实际上并没有完成。“控制器的构造函数被编译成'void'方法,这就是为什么我得到错误。我对吗? – dyatchenko 2015-03-02 10:04:36

+0

@dyatchenko号。这与'async void'无关。 ASP.NET知道如何处理'async Task'。因为它注册到'SynchronizationContext',它注意到你的'DownloadAsync'方法在你的控制器返回时没有完成。 – 2015-03-02 10:05:55

+0

好的。在这种情况下,正如你所提到的,我可以等几秒钟,而我的下载完成后,我可以刷新页面,它应该没问题,但事实并非如此。 – dyatchenko 2015-03-02 10:14:49

1

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

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

1

myWebClient.DownloadStringTaskAsync方法在单独的线程上运行并且是非阻塞的。一个可能的解决方案是使用DownloadDataCompleted事件处理程序为myWebClient和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(); 
} 
0

方法的返回async TaskConfigureAwait(false)可以成为解决方案之一。它会像异步无效,无法继续同步上下文(只要你真的不关心该方法的最终结果)

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

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

在您的代码中,您返回但未确保DownloadAsync()已运行完毕。但是,您将结果保存到task成员,因此确保完成此过程非常简单。简而言之await task在所有的动作方法(asyncifying他们以后)之前返回:

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

编辑:

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