Урок 13. Дочерние задачи C#


Тринадцатый урок учебника по параллельному программированию в .NET описывает использование вложенных дочерних задач. Как и вложенные задачи, они создаются в рамках существующей параллельной задачи. Разница в том, что эти задачи связаны с их родителями.

Что такое дочерняя задача?

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

Дочерняя задача создается в делегате родительской задачи аналогично вложенной задаче. Ключевое отличие состоит в том, что дочерняя задача связана с родительской несколькими способами. Ссылка автоматически предоставляет две ключевые функции:

Родительская задача не завершит выполнение до тех пор, пока не завершатся все ее дочерние задачи, как обычно, так и с исключениями. Родитель, по сути, выполняет команду ожидания для всех своих потомков.

Если дочерняя задача создает исключение, которое в противном случае не обрабатывается, она захватывается родительской задачей и перестраивается. Для многих дочерних задач возможно создать исключение. Все они будут объединены с любым необработанным исключением, вызванным родителем непосредственно в одном AggregateException.

Примечание: вышеприведенные пункты не дают исчерпывающего списка различий между вложенной задачей и дочерней задачей.

Создание дочерних задач

На этом уроке мы создадим несколько вложенных дочерних задач, используя примеры, аналогичные примерам из статьи "вложенные задачи". Чтобы упростить ссылки на ключевые классы, добавьте следующие директивы using:

using System.Threading;
using System.Threading.Tasks;
Когда вы создаете задачу в области действия другого лямбда-выражения, она по умолчанию является вложенной. Чтобы связать дочернюю задачу с родительской, необходимо добавить в ее конструктор второй параметр. Это позволяет определить параметры создания задачи. Чтобы прикрепить задачу, вы должны передать значение TaskCreationOptions.AttachedToParent. Это показано в приведенном ниже примере кода, где параметр добавляется к конструкторам объектов loadUserPermissionsTask и loadUserConfigurationTask после лямбда-выражений, определяющих задачи.

var loadUserDataTask = new Task(() =>
{
    Console.WriteLine("Loading User Data");
    Thread.Sleep(2000);
    string userID = "1234";
    Console.WriteLine("User ID loaded");
 
    var loadUserPermissionsTask = new Task(() =>
    {
        Console.WriteLine("Loading User Permissions for user {0}", userID);
        Thread.Sleep(2000);
        Console.WriteLine("User permissions loaded");
    }, TaskCreationOptions.AttachedToParent);
    loadUserPermissionsTask.Start();
 
    var loadUserConfigurationTask = new Task(() =>
    {
        Console.WriteLine("Loading User Configuration for user {0}", userID);
        Thread.Sleep(2000);
        Console.WriteLine("User configuration loaded");
    }, TaskCreationOptions.AttachedToParent);
    loadUserConfigurationTask.Start();
 
    Console.WriteLine("Parent task finished");
});
 
loadUserDataTask.Start();
loadUserDataTask.Wait();
loadUserDataTask.Dispose();
 
/* OUTPUT
 
Loading User Data
User ID loaded
Parent task finished
Loading User Permissions for user 1234
Loading User Configuration for user 1234
User configuration loaded
User permissions loaded
Когда приведенный выше код выполняется, родительская задача запускает обе дочерние задачи. Прежде чем родительская задача завершится, она ожидает завершения двух дочерних задач. Вы можете увидеть это в выходных данных. Если бы дочерние задачи не ждали, их сообщения не отображались бы, так как программа завершилась бы раньше, чем они успели бы выполнить.

Обработка исключений в дочерних задачах

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

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

var loadUserDataTask = new Task(() =>
{
    Console.WriteLine("Loading User Data");
    Thread.Sleep(2000);
    string userID = "1234";
    Console.WriteLine("User ID loaded");
 
    var loadUserPermissionsTask = new Task(() =>
    {
        Console.WriteLine("Loading User Permissions for user {0}", userID);
        Thread.Sleep(2000);
        throw new Exception("User permissions could not be loaded");
    }, TaskCreationOptions.AttachedToParent);
    loadUserPermissionsTask.Start();
 
    var loadUserConfigurationTask = new Task(() =>
    {
        Console.WriteLine("Loading User Configuration for user {0}", userID);
        Thread.Sleep(2000);
        throw new Exception("User configuration could not be loaded");
    }, TaskCreationOptions.AttachedToParent);
    loadUserConfigurationTask.Start();
 
    throw new Exception("Parent task failed");
});
 
loadUserDataTask.Start();
 
try
{
    loadUserDataTask.Wait();
}
catch (AggregateException ex)
{
    foreach (var exception in ex.Flatten().InnerExceptions)
    {
        Console.WriteLine(exception.Message);
    }
}
 
loadUserDataTask.Dispose();
 
/* OUTPUT
 
Loading User Data
User ID loaded
Loading User Permissions for user 1234
Loading User Configuration for user 1234
Parent task failed
User permissions could not be loaded
User configuration could not be loaded
 
*/
Автор этого материала - я - Пахолков Юрий. Я оказываю услуги по написанию программ на языках Java, C++, C# (а также консультирую по ним) и созданию сайтов. Работаю с сайтами на CMS OpenCart, WordPress, ModX и самописными. Кроме этого, работаю напрямую с JavaScript, PHP, CSS, HTML - то есть могу доработать ваш сайт или помочь с веб-программированием. Пишите сюда.

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




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




2D платформер на Unity: управляем котом с помощью джойстика
Какой язык лучше для разработки игр?
Миллион под ногами