Почему всегда распространяется только одно из многих исключений из дочерних задач?

Я изо всех сил пытаюсь лучше понять логику обработки исключений и ошибок в TPL (и еще немного повезло в задачах async / await .NET 4.5)

Слегка измененный из моего предыдущего вопроса «Как лучше понять код / ​​операторы из статьи« Асинхронный режим - обработка нескольких исключений »?» Код консольного приложения C #, запускающий 2 отдельных внутренних вложенных прикрепленных (зависимых) дочерних (обновленных) (Обновление: извините , начал один вопрос, а закончил другим!) задачи:

class Program
{  
   static void Main(string[] args)
   {  Tst();
      Console.ReadLine();
   }
   async static Task  Tst()
   {
       try
       {
           await Task.Factory.StartNew
             (() =>
                {
                   Task.Factory.StartNew
                       (   () => { 
                                    Console.WriteLine("From 1st child");
                                    throw new NullReferenceException(); 
                                  }
                            , TaskCreationOptions.AttachedToParent
                        );
               Task.Factory.StartNew
                       (  () =>
                               { 
                                   Console.WriteLine("From 2nd child");
                                   throw new ArgumentException(); 
                               }
      ,TaskCreationOptions.AttachedToParent
                       );
                }
             );
    }
    catch (AggregateException ex)
    {
        Console.WriteLine("** {0} **", ex.GetType().Name);
        foreach (var exc in ex.Flatten().InnerExceptions)
        {
             Console.WriteLine(exc.GetType().Name);
        }
    }
    catch (Exception ex)
    {
       Console.WriteLine("## {0} ##", ex.GetType().Name);
    }
 } 

производит вывод, который чередуется (недетерминированно) между:

From 1st child
From 2nd child
** AggregateException **
ArgumentException

а также

From 1t child
From 2nd child
** AggregateException **
NullReferenceException

Похоже, что всегда одно и только одно исключение из одной из дочерних задач всегда распространяется / перехватывается.

Почему распространяется / перехватывается только одно исключение?
Я бы лучше понял, если бы ни одно, а точнее, все исключения из дочерних задач всегда перехватывались

Возможно ли в этой ситуации, что оба исключения или ни одно исключение не будут обнаружены?


person Gennady Vanin Геннадий Вани&    schedule 16.05.2013    source источник


Ответы (2)


Не следует смешивать родительские / дочерние задачи с async. Они не были созданы для совместной работы.

svick уже ответил на этот вопрос в рамках своего (правильного) ответа на ваш другой вопрос. Вот как это можно представить:

  • Каждый внутренний StartNew получает одно исключение, которое упаковывается в AggregateException и помещается в возвращенный Task.
  • Внешний StartNew получает оба AggregateException из своих дочерних задач, которые он переносит в другой AggregateException при возвращении Task.
  • Когда вы await a Task, возникает первое внутреннее исключение. Любые другие игнорируются.

Вы можете наблюдать за этим поведением, сохранив Task и проверив их после того, как исключение будет вызвано await:

async static Task Test()
{
    Task containingTask, nullRefTask, argTask;
    try
    {
        containingTask = Task.Factory.StartNew(() =>
        {
            nullRefTask = Task.Factory.StartNew(() =>
            {
                throw new NullReferenceException();
            }, TaskCreationOptions.AttachedToParent);
            argTask = Task.Factory.StartNew(() =>
            {
                throw new ArgumentException();
            }, TaskCreationOptions.AttachedToParent);
        });
        await containingTask;
    }
    catch (AggregateException ex)
    {
        Console.WriteLine("** {0} **", ex.GetType().Name);
    }
}

Если вы установите точку останова на WriteLine, вы увидите, что исключения из обеих дочерних задач помещаются в родительскую задачу. Оператор await распространяет только один из них, поэтому вы поймаете только один.

person Stephen Cleary    schedule 16.05.2013
comment
Подробнее о причинах такого поведения см. Во второй половине статьи Стивена Туба Обработка исключений задачи в .NET 4.5. - person svick; 16.05.2013

Из того, что я могу понять, причина, по которой это происходит, заключается в том, что await сигнализирует этой задаче, что нужно дождаться ее завершения. Когда генерируется исключение, задача завершается (поскольку исключение приводит к ее сбою), и исключение распространяется на вашу асинхронную функцию, где оно будет перехвачено. Это означает, что при такой настройке вы всегда будете ловить одно исключение.

Чтобы всегда ловить оба, удалите ожидание и вместо этого используйте Task.Factory.StartNew (..). Wait (); Функция ожидания будет вести подсчет всех дочерних процессов и не вернется, пока все они не закончатся. Поскольку генерируется несколько исключений (по одному от каждого дочернего элемента), они объединяются в новое исключение AggregateException, которое позже перехватывается, его дочерние элементы сглаживаются, а внутренние исключения печатаются. Это должно дать вам результат:

From 1st child
From 2nd child
** AggregateException **
ArgumentException
NullReferenceException
person Flipbed    schedule 16.05.2013