Урок 12. Вложенные задачи C#


На двенадцатом уроке учебника по параллельному программированию в .NET рассматривается возможность вложенности задач. Вложенная задача - это задача, которая создается и выполняется внутри делегата другой задачи. Вложенные задачи не связаны со своими родительскими задачами.

Что такое вложенная задача?

При разработке параллельных приложений обычно запускаются новые задачи из делегата существующей задачи. Задачи могут быть вложены таким образом на много уровней глубже. Внутренние задачи известны как дочерние задачи, которые делятся на два типа. Первый тип-это отделенная дочерняя задача, на которой и сосредоточена данная статья. Эти задачи также известны как вложенные задачи. Другой тип дочерней задачи - это прикрепленная дочерняя задача, обычно известная просто как дочерние задачи. Мы рассмотрим их на следующем уроке учебника. Поскольку эти два типа дочерних задач имеют схожие названия, я буду называть отдельные дочерние задачи вложенными задачами, а прикрепленные дочерние задачи - дочерними задачами для остальной части учебника.

При создании вложенных задач нет никакой связи между вложенной задачей и ее родителем. Вложенные задачи полностью независимы, сообщают об отдельном статусе и выдают свои собственные исключения. Планирование родительских и вложенных задач также разделено; родительские задачи могут быть завершены до того, как их вложенные задачи начнутся.

В приведенном ниже коде показана родительская задача с двумя вложенными задачами. Вы можете видеть, что вложенные задачи создаются и запускаются в делегате внешней задачи. При выполнении родительская задача, вероятно, завершится еще до того, как вложенные задачи будут запущены. Это показывает, что планирование задач не связано. Метод Console.ReadLine включен для того, чтобы дать вложенным задачам возможность закончить работу и вывести их сообщения. Мы не можем ждать вложенных задач вне родительской задачи, так как переменные loadUserPremissionTask и loadUserConfigurationTask на данный момент находятся вне области видимости.

var loadUserDataTask = new Task(() =>
{
    Console.WriteLine("Загрузка пользовательских данных");
    Thread.Sleep(2000);
    string userID = "1234";
    Console.WriteLine("Идентификатор пользователя загружен");
 
    var loadUserPermissionsTask = new Task(() =>
    {
        Console.WriteLine("Загрузка пользовательских разрешений для пользователя {0}", userID);
        Thread.Sleep(2000);
        Console.WriteLine("Загружены разрешения пользователя");
    });
    loadUserPermissionsTask.Start();
 
    var loadUserConfigurationTask = new Task(() =>
    {
        Console.WriteLine("Загрузка конфигурации пользователя для пользователя {0}", userID);
        Thread.Sleep(2000);
        Console.WriteLine("Загружена конфигурация пользователя");
    });
    loadUserConfigurationTask.Start();
 
    Console.WriteLine("Родительская задача завершена");
});
 
loadUserDataTask.Start();
loadUserDataTask.Wait();
loadUserDataTask.Dispose();
 
Console.ReadLine();
 

/* вывод
             
Загрузка пользовательских данных
Идентификатор пользователя загружен
Родительская задача завершена
Загрузка пользовательских разрешений для пользователя 1234
Загрузка конфигурации пользователя для пользователя 1234
Загружены разрешения пользователя 
Загружена конфигурация пользователя
              
*/
Исключения во вложенных задачах

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

Мы можем продемонстрировать это на следующем примере. Здесь вложенные задачи являются модифицированными версиями задач из первого примера. На этот раз они оба делают исключения. Родительская задача также создает исключение, и только оно попадает в блок try/catch, окружающий оператор Wait. Поскольку ни один метод не вызывается для ожидания вложенных задач, их исключения остаются необработанными. Вот почему мы видим только одно внутреннее исключение в пойманном AggregateException.

var loadUserDataTask = new Task(() =>
{
    Console.WriteLine("Загрузка пользовательских данных");
    Thread.Sleep(2000);
    string userID = "1234";
    Console.WriteLine("Идентификатор пользователя загружен");
 
    var loadUserPermissionsTask = new Task(() =>
    {
        Console.WriteLine("Загрузка пользовательских разрешений для пользователя {0}", userID);
        Thread.Sleep(2000);
        throw new Exception("Загружены разрешения пользователя ");
    });
    loadUserPermissionsTask.Start();
 
    var loadUserConfigurationTask = new Task(() =>
    {
        Console.WriteLine("Загрузка конфигурации пользователя для пользователя {0}", userID);
        Thread.Sleep(2000);
        throw new Exception("Конфигурация не загружена");
    });
    loadUserConfigurationTask.Start();
 
    throw new Exception("Родительская задача не удалась");
});
 
loadUserDataTask.Start();
 
try
{
    loadUserDataTask.Wait();
}
catch (AggregateException ex)
{
    foreach (var exception in ex.InnerExceptions)
    {
        Console.WriteLine(exception.Message);
    }
}
 
loadUserDataTask.Dispose();
 
Console.ReadLine();
 
/*
             
Загрузка Пользовательских Данных
Идентификатор пользователя загружен
Загрузка пользовательских разрешений для пользователя 1234
Загрузка конфигурации пользователя для пользователя 1234
Родительская задача не удалась
              
*/
Перехват вложенных исключений задач

Чтобы перехватывать исключения из вложенных задач, необходимо добавить ожидание их завершения в родительской задаче. Это предотвратит выход родительской задачи до завершения выполнения вложенных задач. Если методы ожидания вызываются в блоке try/catch, вы можете получить исключения и либо агрегировать их, либо обрабатывать. Если вы опустите блок try/catch, исключения приведут к сбою родительской задачи, и исключения будут включены в вызываемое ею исключение AggregateException.

В следующем упрощенном примере создается внешняя задача и две вложенные задачи. Каждая вложенная задача создает исключение, и каждая из них ждет своего решения. Это приводит к тому, что два исключения распространяются через родительскую задачу и, наконец, попадают в блок try/catch, содержащий вызов ожидания внешней задачи. Обратите внимание, что пойманное исключение AggregateException содержит только одно внутреннее исключение, которое само является агрегацией исключений двух дочерних задач. Это происходит потому, что первое AggregateException генерируется вызовом Task.WaitAll внутри родительской задачи. Это обернуто во второе исключение AggregateException вызовом outerTask.Wait.

var outerTask = new Task(() =>
{
    var innerTask1 = new Task(() =>
    {
        throw new Exception("Inner Task 1 Failed");
    });
    innerTask1.Start();
 
    var innerTask2 = new Task(() =>
    {
        throw new Exception("Inner Task 2 Failed");
    });
    innerTask2.Start();
 
    Task.WaitAll(innerTask1, innerTask2);
});
 
outerTask.Start();
 
try
{
    outerTask.Wait();
}
catch (AggregateException ex)
{
    foreach (var exception in ex.InnerExceptions)
    {
        Console.WriteLine(exception.Message);
    }
}
 
/*
             
One or more errors occurred.
              
*/
Сглаживание AggregateExceptions

По мере усложнения иерархии вложенных задач растет и иерархия исключений, которые она может генерировать. Вы можете обнаружить, что ловите AggregateExceptions, содержащие много уровней вложенных исключений. Вы можете написать метод, который сглаживает иерархию внутренних исключений, чтобы получить простой список. К счастью, вам это не нужно, так как он уже существует внутри .NET Framework. Если вы вызываете метод Flatten для AggregateException, возвращается новое AggregateException. Оно содержит все внутренние исключения из всех уровней иерархии. Свойство InnerExceptions нового исключения затем легко перечислить.

Последний пример - это слегка измененная версия предыдущего. На этот раз пойманное исключение сглажено, так что все внутренние исключения могут быть легко отображены.

var outerTask = new Task(() =>
{
    var innerTask1 = new Task(() =>
    {
        throw new Exception("Inner Task 1 Failed");
    });
    innerTask1.Start();
 
    var innerTask2 = new Task(() =>
    {
        throw new Exception("Inner Task 2 Failed");
    });
    innerTask2.Start();
 
    Task.WaitAll(innerTask1, innerTask2);
});
 
outerTask.Start();
 
try
{
    outerTask.Wait();
}
catch (AggregateException ex)
{
    foreach (var exception in ex.Flatten().InnerExceptions)
    {
        Console.WriteLine(exception.Message);
    }
}
 
/*
             
Inner Task 1 Failed
Inner Task 2 Failed
              
*/
Автор этого материала - я - Пахолков Юрий. Я оказываю услуги по написанию программ на языках Java, C++, C# (а также консультирую по ним) и созданию сайтов. Работаю с сайтами на CMS OpenCart, WordPress, ModX и самописными. Кроме этого, работаю напрямую с JavaScript, PHP, CSS, HTML - то есть могу доработать ваш сайт или помочь с веб-программированием. Пишите сюда.

тегистатьи IT, параллельное программирование, си шарп, tasks, исключения




Отправляя сообщение я подтверждаю, что ознакомлен и согласен с политикой конфиденциальности данного сайта.




Шифрование строки с помощью C# и SHA-1
Лабиринты Java, часть 2: класс Waypoint
Липовый счетчик лиру: межстрочный интервал и наведение