【问题标题】:Why doesn't this exception get thrown?为什么不抛出这个异常?
【发布时间】:2014-07-09 03:05:29
【问题描述】:

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

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;
 });
}

我不明白为什么异常永远不会出现在任何地方。我会想到,如果等待带有异常的任务,它会抛出。我可以通过用一个简单的 for each 替换 while 循环来确认这一点,如下所示:

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

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

【问题讨论】:

  • 有什么理由不使用Task.WhenAll
  • @PauloMorgado - 是的,这些与托管资源相关联,我想在它们可用时释放它们,而不是等待它们全部完成然后释放。
  • 此资源发布不在您发布的代码上,对吧?并且托管资源“释放自己”。
  • @PauloMorgado - 版本不在代码中,正确。至于托管资源说明,抱歉打错了,它应该读取“非托管”,因为它的生命周期由 using 块决定并继承 IDisposable。

标签: c# .net exception async-await task


【解决方案1】:

Task 不是 awaited 或未使用其 Wait()Result() 方法,默认情况下会吞下异常。一旦Task 被 GC'd,通过使正在运行的进程崩溃,可以将此行为修改回它在 .NET 4.0 中完成的方式。您可以在app.config 中设置如下:

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

引用来自 Microsoft 并行编程团队的 this 博客文章:

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

【讨论】:

  • 我相信 Yuval 的回答解释了您所看到的行为。此外,在 run() 返回的任务对象中应该可以访问异常。
【解决方案2】:

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


WhenAny 的 MSDN 文档指出:

当任何提供的任务完成时,返回的任务将完成。返回的任务将始终以 RanToCompletion 状态结束,其 Result 设置为要完成的第一个任务。即使第一个要完成的任务以 CancelledFaulted 状态结束也是如此。

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

【讨论】:

    【解决方案3】:

    来自评论:

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

    使用帮助器 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 global exception handler

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2017-07-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-04-22
      相关资源
      最近更新 更多