为什么不抛出这个异常?

Tra*_*s J 8 .net c# exception task async-await

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

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

然后像这样调用它:

await ReleaseAsync(task1, task2, task3);
//or
await ReleaseAsync(tasks.ToArray());
Run Code Online (Sandbox Code Playgroud)

但是,最近我注意到了一些奇怪的行为,并设置了ReleaseAsync方法是否存在问题.我设法将它缩小到这个简单的演示,如果你包含它,它将在linqpad中运行System.Threading.Tasks.它也可以在控制台应用程序或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;
 });
}
Run Code Online (Sandbox Code Playgroud)

我不明白为什么异常永远不会出现在任何地方.我想如果等待具有异常的任务,它就会抛出.我能够通过用这样的简单替换while循环来确认这一点:

foreach( var task in TaskArray )
{
  await task;//this will throw the exception properly
}
Run Code Online (Sandbox Code Playgroud)

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

Ken*_*rey 11

TL; DR:run()抛出异常,但是你正在等待WhenAny(),它本身不会抛出异常.


状态的MSDN文档WhenAny:

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

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


Yuv*_*kov 10

一个Task不是awaited或不使用它Wait()Result()方法,将默认吞例外.这种行为可以通过崩溃运行过程一旦Task被GC'd 修改回原来在.NET 4.0中完成的方式.您可以app.config按如下方式设置它:

<configuration> 
    <runtime> 
        <ThrowUnobservedTaskExceptions enabled="true"/> 
    </runtime> 
</configuration>
Run Code Online (Sandbox Code Playgroud)

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

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