为什么不抛出这个exception?

我有时会使用一组任务,为了确保它们都在等待我使用这种方法:

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

我的问题是,为什么显示的示例没有正确抛出exception(它永远不会出现在任何地方)。

TL; DRrun()抛出exception,但是你正在等待WhenAny() ,它本身不会抛出exception。


WhenAny的MSDN文档说明:

任何提供的任务完成后,返回的任务将完成。 返回的任务将始终以RanToCompletion状态结束,并将其Result设置为要完成的第一个任务。 即使完成的第一个任务以“ 已取消”或“ 故障”状态结束,也是如此。

基本上发生的事情是WhenAny返回的任务只是吞下了故障任务。 它只关心任务完成的事实,而不是它已成功完成。 当您等待任务时,它只是完成而没有错误,因为它是内部任务出错,而不是您正在等待的任务。

awaited或未使用其Wait()Result()方法的任务将默认吞下该exception。 通过在Task为GC后崩溃正在运行的进程,可以将此行为修改回在.NET 4.0中完成的方式。 您可以在app.config进行设置,如下所示:

      

微软并行编程团队在这篇博文中的引用:

那些熟悉.NET 4中的任务的人会知道TPL具有“未观察到的”exception的概念。 这是TPL中两个竞争设计目标之间的折衷:支持将异步操作中的未处理exception编组到消耗其完成/输出的代码,并遵循应用程序代码未处理的exception的标准.NETexception升级策略。 从.NET 2.0开始,在新创建的线程,ThreadPool工作项等中未处理的exception都会导致默认的exception升级行为,这会导致进程崩溃。 这通常是可取的,因为exception表明出现了问题,并且崩溃有助于开发人员立即识别应用程序已进入不可靠状态。 理想情况下,任务将遵循相同的行为。 但是,任务用于表示稍后加入的异步操作,如果这些异步操作产生exception,则应将这些exception封送到加入代码的运行位置并消耗异步操作的结果。 这固有地意味着TPL需要支持这些exception并保持它们,直到消费代码访问任务时它们可以被再次抛出。 由于这会阻止默认升级策略,因此.NET 4应用了“未观察到的”exception的概念来补充“未处理”exception的概念。 “未观察到的”exception是存储在任务中但从未以消费代码以任何方式查看的exception。 有许多方法可以观察exception,包括Wait()在Task上,访问Task的Result,查看Task的Exception属性,等等。 如果代码从未观察到Task的exception,那么当Task消失时,会引发TaskScheduler.UnobservedTaskException,为应用程序提供了另一个“观察”exception的机会。 如果exception仍未被观察到,则exception升级策略将由终结器线程上未处理的exception启用。

来自评论:

这些[任务]与托管资源相关联,我希望在它们可用时释放它们,而不是等待所有这些资源完成然后发布。

使用帮助程序async void方法可以为您提供所需的行为,从列表中删除已完成的任务并立即抛出未观察到的exception:

 public static class TaskExt { public static async void Observe(Task task) { await task; } public static async Task WithObservation(Task 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(TaskArray); while (tasks.Any()) tasks.Remove(await Task.WhenAny(tasks)); } 

.Observe()将立即“带外”重新抛出任务的exception,如果调用线程具有同步上下文,则使用SynchronizationContext.Post ,否则使用ThreadPool.QueueUserWorkItem 。 您可以使用AppDomain.CurrentDomain.UnhandledException处理此类“带外”exception。

我在这里详细描述了这个:

TAP全局exception处理程序