Урок 11. Задачи продолжения C#


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

Объединение параллельных задач

Когда вы пишете программное обеспечение, в котором есть задачи, выполняющиеся параллельно, часто возникают еще параллельные задачи, которые зависят от результатов других. Эти задачи не следует запускать до тех пор, пока не будут выполнены предыдущие задачи, известные как предшествующие. До появления библиотеки параллельных задач (TPL) этот тип взаимозависимого выполнения потоков контролировался с помощью обратных вызовов. Здесь вызывается метод, и один из его параметров предоставляется делегату для выполнения после завершения задачи.

Хотя обратные вызовы обеспечивают жизнеспособное решение многих проблем зависимостей параллельного программирования, они могут очень быстро стать сложными. Для управления многопоточностью требуется дополнительный шаблонный код, что может привести к запутанному коду. Эта проблема усугубляется, когда у вас есть сложные зависимости между задачами. Например, когда одна задача должна быть выполнена только после выполнения нескольких других.

При использовании классов TPL и Task более простое решение существует в задачах продолжения. Эти задачи связаны со своими предыдущими задачами и автоматически запускаются после завершения более ранних задач. В этой статье мы рассмотрим основы продолжения. Мы будем использовать классы из пространств имен System.Threading и System.Threading.Tasks, поэтому, если вы хотите воссоздать примеры, вам следует создать новое консольное приложение и добавить в код следующие директивы using:

using System.Threading;
using System.Threading.Tasks;
Создание задач продолжения

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

Синтаксис метода ContinueWith следующий:

Task continuation = firstTask.ContinueWith(antecedent =>{/ * функциональность * /});
Создадим наш первый пример. В приведенном ниже коде мы моделируем два чтения данных из базы данных или другого хранилища данных. Первая задача определяется, как мы видели ранее в учебнике. Он имитирует чтение пользовательских данных для получения идентификатора пользователя, который хранится в переменной userID. Вторая задача - продолжение. Он имитирует загрузку информации о разрешениях пользователя, используя идентификатор, полученный в предшествующей задаче. В этом случае параметр лямбда-выражения остается неиспользованным. Вызывается метод продолжения Wait, чтобы обеспечить выполнение обеих задач до отображения последнего сообщения.

Когда вы запустите код, вы увидите, что loadUserPermissionsTask не запустится, пока loadUserDataTask не будет завершен.

string userID = null;
 
var loadUserDataTask = new Task(() =>
{
    Console.WriteLine("Loading User Data");
    Thread.Sleep(2000);
    userID = "1234";
    Console.WriteLine("User data loaded");
});
 
var loadUserPermissionsTask = loadUserDataTask.ContinueWith(t =>
{
    Console.WriteLine("Loading User Permissions for user {0}", userID);
    Thread.Sleep(2000);
    Console.WriteLine("User permissions loaded");
});
 
loadUserDataTask.Start();
loadUserPermissionsTask.Wait();
 
Console.WriteLine("CRM Application Loaded");
 
loadUserDataTask.Dispose();
loadUserPermissionsTask.Dispose();
 
/* OUTPUT
 
Loading User Data
User data loaded
Loading User Permissions for user 1234
User permissions loaded
CRM Application Loaded
 
*/
Использование результатов задачи в продолжениях

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

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

var loadUserDataTask = new Task<string>(() =>
{
    Console.WriteLine("Loading User Data");
    Thread.Sleep(2000);
    Console.WriteLine("User data loaded");
    return "1234";
});
 
var loadUserPermissionsTask = loadUserDataTask.ContinueWith(t =>
{
    Console.WriteLine("Loading User Permissions for user {0}", t.Result);
    Thread.Sleep(2000);
    Console.WriteLine("User permissions loaded");
    return "Admin";
});
 
loadUserDataTask.Start();
 
Console.WriteLine("CRM Application Loaded for {0}", loadUserPermissionsTask.Result);
 
loadUserDataTask.Dispose();
loadUserPermissionsTask.Dispose();
 
/* OUTPUT
 
Loading User Data
User data loaded
Loading User Permissions for user 1234
User permissions loaded
CRM Application Loaded for Admin
 
*/
Множественные продолжения одного предшествующего

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

Пример ниже демонстрирует это путем добавления к предыдущему примеру. Здесь loadUserDataTask используется для получения идентификатора пользователя. Две задачи продолжения, loadUserPermissionsTask и loadUserConfigurationTask, используют полученное значение для загрузки правильных разрешений и параметров конфигурации для пользователя.

В этом случае метод Task.WaitAll используется для ожидания завершения обоих продолжений перед отображением последнего сообщения.

var loadUserDataTask = new Task<string>(() =>
{
    Console.WriteLine("Loading User Data");
    Thread.Sleep(2000);
    Console.WriteLine("User data loaded");
    return "1234";
});
 
var loadUserPermissionsTask = loadUserDataTask.ContinueWith(t =>
{
    Console.WriteLine("Loading User Permissions for user {0}", t.Result);
    Thread.Sleep(2000);
    Console.WriteLine("User permissions loaded");
});
 
var loadUserConfigurationTask = loadUserDataTask.ContinueWith(t =>
{
    Console.WriteLine("Loading User Configuration for user {0}", t.Result);
    Thread.Sleep(2000);
    Console.WriteLine("User configuration loaded");
});
 
loadUserDataTask.Start();
Task.WaitAll(loadUserPermissionsTask, loadUserConfigurationTask);
 
Console.WriteLine("CRM Application Loaded");
 
loadUserDataTask.Dispose();
loadUserPermissionsTask.Dispose();
loadUserConfigurationTask.Dispose();
 
/* OUTPUT
 
Loading User Data
User data loaded
Loading User Permissions for user 1234
Loading User Configuration for user 1234
User configuration loaded
User permissions loaded
CRM Application Loaded
 
*/
Создание продолжений с несколькими антецедентами

В другом сложном сценарии у вас есть задача продолжения, которая зависит от выполнения нескольких предшествующих. Это тоже возможно с помощью TPL, но без использования метода ContinueWith. Вместо этого вы можете использовать статический метод Task.Factory.ContinueWhenAll. Он принимает массив объектов Task в качестве первого параметра, все из которых должны быть выполнены, прежде чем новая задача может быть запланирована. Лямбда-выражение, определяющее продолжение, является вторым аргументом. Массив антецедентов передается делегату, который имеет тип Action <Task []>. Продолжение возвращается.

Следующий пример расширяет предыдущий. После завершения задач loadUserPermissionsTask и loadUserConfigurationTask будет запланирована finalTask. Последняя задача показывает результаты предшествующих задач после извлечения их из массива.

var loadUserDataTask = new Task<string>(() =>
{
    Console.WriteLine("Loading User Data");
    Thread.Sleep(2000);
    Console.WriteLine("User data loaded");
    return "1234";
});
 
var loadUserPermissionsTask = loadUserDataTask.ContinueWith(t =>
{
    Console.WriteLine("Loading User Permissions for user {0}", t.Result);
    Thread.Sleep(2000);
    Console.WriteLine("User permissions loaded");
    return "Admin";
});
 
var loadUserConfigurationTask = loadUserDataTask.ContinueWith(t =>
{
    Console.WriteLine("Loading User Configuration for user {0}", t.Result);
    Thread.Sleep(2000);
    Console.WriteLine("User configuration loaded");
    return "Rich UI";
});
 
var dependencies = new Task[] { loadUserPermissionsTask, loadUserConfigurationTask };
var finalTask = Task.Factory.ContinueWhenAll(dependencies, t =>
{
    Console.WriteLine("\nCRM Application Loaded");
    Console.WriteLine("User permissions : {0}", ((Task)t[0]).Result);
    Console.WriteLine("User experience  : {0}", ((Task)t[1]).Result);
});
 
loadUserDataTask.Start();
finalTask.Wait();
 
loadUserDataTask.Dispose();
loadUserPermissionsTask.Dispose();
loadUserConfigurationTask.Dispose();
finalTask.Dispose();
 
/* OUTPUT
 
Loading User Data
User data loaded
Loading User Permissions for user 1234
Loading User Configuration for user 1234
User permissions loaded
User configuration loaded
 
CRM Application Loaded
User permissions : Admin
User experience  : Rich UI
 
*/
Обработка исключений с продолжениями

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

Важно понимать, что нет никакой связи между антецедентами и продолжениями, кроме той, которая контролирует планирование. В частности, исключения, создаваемые задачей, не распространяются на связанные задачи продолжения. Это означает, что вы должны проверять исключения во всех выполняемых задачах. Самое простое решение - дождаться завершения всех задач, которые могут привести к ошибке, и окружить метод Wait блоком Try / Catch. Как и в случае с другими параллельными конструкциями, исключения заключаются в AggregateException.

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

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

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




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



Изменение размера массивов
Настройки приложения C#: определение, пример
Подключение Xdebug и NetBeans