为什么不抛出此异常

本文关键字:异常 为什么不 | 更新日期: 2023-09-27 18:04:12

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

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方法是否存在问题。我设法将其缩小到这个简单的演示,如果您包含 linqpad 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;
 });
}

我不明白的是为什么异常从未出现在任何地方。我会认为,如果等待异常任务,它会抛出。我能够通过将 while 循环替换为每个简单的循环来确认这一点,如下所示:

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

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

为什么不抛出此异常

TL;DRrun()抛出异常,但您正在等待WhenAny(),它本身不会抛出异常。


WhenAny的 MSDN 文档指出:

当提供的任何任务完成时,返回的任务将完成。返回的任务将始终以 RanToComplete(RanToComplete(状态结束,其结果设置为要完成的第一个任务。即使要完成的第一个任务以">已取消"或"故障"状态结束,也是如此。

本质上,正在发生的事情是WhenAny返回的任务只是吞下错误的任务。它只关心任务已完成的事实,而不是它已经成功完成的事实。当您等待任务时,它只会顺利完成,因为出错的是内部任务,而不是您正在等待的任务。

默认情况下,

awaited或未使用其Wait()Result()方法的Task将吞噬异常。此行为可以修改回在 .NET 4.0 中执行的方式,方法是在 GC Task后使正在运行的进程崩溃。您可以在app.config中按如下方式设置它:

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

并行编程团队在Microsoft的这篇博文中引用了一句话:

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

从评论:

这些[任务]与托管资源相关联,我想在以下情况下释放它们 它们变得可用,而不是等待所有完成 然后释放。

使用帮助程序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,否则使用 ThreadPool.QueueUserWorkItem。您可以使用 AppDomain.CurrentDomain.UnhandledException 处理此类"带外"异常。

我在这里更详细地描述了这一点:

TAP 全局异常处理程序