2014-05-20 137 views
8

我用一組任務的時間,爲了確保他們都期待我用這個辦法:爲什麼這個異常不會被拋出?

public async Task ReleaseAsync(params Task[] TaskArray) 
{ 
    var tasks = new HashSet<Task>(TaskArray); 
    while (tasks.Any()) tasks.Remove(await Task.WhenAny(tasks)); 
} 

,然後調用它像這樣:

await ReleaseAsync(task1, task2, task3); 
//or 
await ReleaseAsync(tasks.ToArray()); 

然而,最近我一直注意到一些奇怪的行爲,並設置了ReleaseAsync方法是否有問題。我設法縮小到這個簡單的演示,它包含System.Threading.Tasks,它運行在linqpad。它也將在控制檯應用程序或asp.net mvc控制器中稍作修改。

async void Main() 
{ 
Task[] TaskArray = new Task[]{run()}; 
var tasks = new HashSet<Task>(TaskArray); 
while (tasks.Any<Task>()) tasks.Remove(await Task.WhenAny(tasks)); 
} 

public async Task<int> run() 
{ 
return await Task.Run(() => { 
    Console.WriteLine("started"); 
    throw new Exception("broke"); 
    Console.WriteLine("complete"); 
    return 5; 
}); 
} 

我不明白的是爲什麼Exception從不出現在任何地方。我會認爲,如果有例外的任務等待,它會拋出。我可以用一個簡單的爲每個這樣的更換while循環,以確認這一點:

foreach(var task in TaskArray) 
{ 
    await task;//this will throw the exception properly 
} 

我的問題是,爲什麼不所示的例子拋出異常正確(它從來沒有在任何地方顯示出來)。

+0

的可能重複(http://stackoverflow.com/questions/22856052/how- to-handle-task-factory-startnew-exception) –

+0

任何不使用Task.WhenAll的理由? –

+0

@PauloMorgado - 是的,這些被綁定到託管資源,我想在釋放他們時,而不是等待他們完成,然後釋放。 –

回答

9

TL; DRrun()拋出異常,但你等待WhenAny(),不拋出異常本身。


MSDN文檔WhenAny狀態:當任何的供應任務已完成

返回的任務將完成。返回的任務將始終以RanToCompletion狀態結束,並將其結果設置爲第一個要完成的任務。即使第一項任務完成,也是如此,在已取消Faulted狀態下結束。

本質上,發生的事情是由WhenAny返回的任務只是吞下故障任務。它只關心任務完成的事實,而不是它已經成功完成。當你等待任務時,它完成沒有錯誤,因爲它是內部任務發生故障,而不是你正在等待的那個。

10

A Task不是awaited或不使用其Wait()Result()方法,將默認吞下該異常。這種行爲可以修改回.NET 4.0中的方式,一旦Task被GC化,就會崩潰正在運行的進程。

<configuration> 
    <runtime> 
     <ThrowUnobservedTaskExceptions enabled="true"/> 
    </runtime> 
</configuration> 

this博客文章引述由並行編程團隊在微軟:

那些你熟悉.NET 4中的任務將知道,你可以在你app.config如下設置TPL具有「未觀察到」例外的概念。這是TPL中兩個競爭設計目標之間的妥協:支持將未處理的異常從異步操作封送到消耗其完成/輸出的代碼,並對未由應用程序代碼處理的異常遵循標準的.NET異常升級策略。自從.NET 2.0以來,在新創建的線程,ThreadPool工作項目等上未處理的異常都會導致默認的異常升級行爲,這會導致進程崩潰。這通常是可取的,因爲異常表明出現了問題,崩潰有助於開發人員立即確定應用程序已進入不可靠狀態。理想情況下,任務會遵循相同的行爲。但是,任務用於表示代碼後來加入的異步操作,如果這些異步操作引發異常,則應將這些異常封送到加入代碼正在運行的位置,並消耗異步操作的結果。這本質上意味着TPL需要支持這些異常並保留它們,直到消費代碼訪問任務時再次引發它們。由於這阻止了默認升級策略,因此.NET 4應用了「未觀察到」異常的概念來補充「未處理」異常的概念。一個「未觀察到的」異常是存儲在任務中的,但從未被消費代碼以任何方式看待。觀察異常的方法很多,包括Wait()在Task上的訪問,訪問任務的結果,查看Task的Exception屬性等等。如果代碼從不觀察任務的異常,那麼當任務消失時,會引發TaskScheduler.UnobservedTaskException,從而使應用程序有更多機會「觀察」異常。如果異常仍然未被觀察到,則異常升級策略隨後由終結器線程上未處理的異常啓用。

+0

我相信尤瓦爾的回答解釋了你所看到的行爲。此外,該異常應該可以在run()返回的任務對象中訪問。 – Naylor

4

從評論:

這些[任務]被捆綁到管理的資源,我想釋放他們時,他們 成了,而不是等待它們全部完成 ,然後釋放可用。

使用輔助async void方法可以讓你從列表中都刪除完成的任務,並立即拋出未觀察到異常所需的行爲:

public static class TaskExt 
{ 
    public static async void Observe<TResult>(Task<TResult> task) 
    { 
     await task; 
    } 

    public static async Task<TResult> WithObservation(Task<TResult> task) 
    { 
     try 
     { 
      return await task; 
     } 
     catch (Exception ex) 
     { 
      // Handle ex 
      // ... 

      // Or, observe and re-throw 
      task.Observe(); // do this if you want to throw immediately 

      throw; 
     } 
    } 
} 

然後你的代碼可能是這樣的(未經測試):

async void Main() 
{ 
    Task[] TaskArray = new Task[] { run().WithObservation() }; 
    var tasks = new HashSet<Task>(TaskArray); 
    while (tasks.Any<Task>()) tasks.Remove(await Task.WhenAny(tasks)); 
} 

.Observe()將立即重新拋出任務的例外「出帶外」,採用SynchronizationContext.Post如果調用線程擁有同步nization上下文,否則使用ThreadPool.QueueUserWorkItem。您可以使用AppDomain.CurrentDomain.UnhandledException處理這種「帶外」異常)。

我這裏介紹這更多細節:[?如何處理Task.Factory.StartNew例外]

TAP global exception handler

相關問題