2015-03-02 72 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「逃離」。