Урок 14. Отмена задачи C#
На четырнадцатом уроке учебника по параллельному программированию в .NET рассматривается, как отменяются параллельные задачи. Мы рассмотрим остановку отдельных задач, координацию отмены нескольких задач и работу с задачами, которые отменены до их начала.
Токены отмены
Когда вы выполняете длительный процесс в однопоточном программном обеспечении, его обычно просто отменить. При разработке параллельных приложений, где несколько потоков могут выполняться одновременно, может быть гораздо сложнее координировать отмену нескольких связанных процессов. Например, в приложении Windows у вас может быть несколько задач, каждая из которых выполняет файловую операцию. Если пользователь нажмет кнопку "Отмена", вы можете отменить все эти задачи, убедившись, что данные не потеряны, все файлы правильно закрыты и их объекты удалены.
Параллельная библиотека задач упрощает отмену задач с помощью токенов отмены. При создании экземпляра задачи может быть предоставлен токен. Извне задачи другой процесс сигнализирует о том, что требуется отмена, и этот запрос передается через токен. Код в задаче может проверить токен и изящно выйти после выполнения любых операций очистки. Кроме того, токен отмены может совместно использоваться несколькими параллельными задачами, что позволяет одному запросу на отмену остановить выполнение любого количества задач.
На этом уроке мы рассмотрим несколько примеров использования токенов отмены для отмены задач. Мы начнем с построения стандартного шаблона для отмены задачи в несколько этапов. Чтобы воссоздать примеры, создайте новое консольное приложение. Чтобы упростить ссылки на ключевые классы, добавьте следующие директивы using:
using System.Threading; using System.Threading.Tasks;Отмена задачи
Существует несколько требований, позволяющих отменить task. Во-первых, вам нужно сгенерировать токен, который поможет координировать отмену. Сам токен является экземпляром структуры CancellationToken. Маркер не создается с помощью конструктора. Вместо этого вы создаете экземпляр класса CancellationTokenSource и считываете токен из его свойства Token.
При создании задач, поддерживающих отмену, вы передаете маркер конструктору задачи в дополнение к делегату для выполнения. Маркер также должен быть доступен внутри делегата, чтобы вы могли получить доступ к его свойствам и методам. К ним относится свойство IsCancellationRequested, которое возвращает логическое значение, указывающее, была ли запрошена отмена или нет.
Заключительная часть поддержки отмены - это обеспечение того, чтобы ваша задача могла изящно остановиться и оставить систему в рабочем состоянии. В рамках длительных задач вы должны периодически проверять, была ли задача отменена, и, если она была отменена, выполнять какую-либо очистительную работу перед выходом. Это может включать закрытие файлов или подключений к базе данных, завершение или откат транзакций и утилизацию ресурсов. Следует отметить, что запрос на отмену никогда не заставит задачу остановиться. Если вы проигнорируете запрос на отмену, ваша задача будет продолжать выполняться до тех пор, пока не завершится или не возникнет исключение.
Мы можем увидеть этот паттерн в нашем первом примере ниже. В основном методе мы создаем CancellationTokenSource и используем его для получения токена. Затем мы передаем этот токен конструктору нашей задачи. В этом случае лямбда-выражение, определяющее действия задачи, вызывает отдельный метод. Это делается для того, чтобы легче было увидеть структуру шаблона отмены задачи. После запуска задачи мы ждем, пока пользователь нажмет клавишу Enter, прежде чем отменить задачу, вызвав метод Cancellationtokensource Cancel. Это говорит всем задачам, которые использовали его токены, что они должны прекратить выполнение.
Метод DoLongRunningTask вызывается из параллельной задачи. Он имитирует длительный процесс, повторяя цикл сто раз и делая паузу на одну секунду между каждой итерацией. Перед началом цикла проверяется свойство iscancellationrequested токена. Поскольку вполне возможно, что задача могла быть отменена до того, как она фактически начала выполняться, эта проверка позволяет остановить ее без выполнения какой-либо работы. Флаг IsCancellationRequested проверяется снова во время каждой итерации. Если true, то отображается сообщение и цикл завершается.
Попробуйте запустить код и выполнить несколько итераций, прежде чем нажать клавишу Enter для отмены задачи.
static void Main() { var tokenSource = new CancellationTokenSource(); var token = tokenSource.Token; var task = new Task(() => DoLongRunningTask(token), token); Console.WriteLine("нажмите Enter для выхода"); task.Start(); Console.ReadLine(); tokenSource.Cancel(); task.Wait(); task.Dispose(); Console.WriteLine("нажмите Enter для выхода"); Console.ReadLine(); } static void DoLongRunningTask(CancellationToken token) { if (token.IsCancellationRequested) { Console.WriteLine("отменено перед запуском длительной задачи"); return; } for (int i = 0; i <= 100; i++) { Console.WriteLine("{0}%", i); Thread.Sleep(1000); if (token.IsCancellationRequested) { Console.WriteLine("Отменено"); break; } } }Статусы
Существует проблема с вышеуказанным подходом. Когда задача завершается, можно прочитать несколько свойств, чтобы определить ее состояние. При обычном выходе из задачи свойству Status присваивается значение RanToCompletion, а свойству IsCanceled-значение false. Это говорит о том, что задание выполнено нормально и не было отменено.
Чтобы увидеть свойства, измените основной метод, как показано ниже, и снова запустите код. Последние три выведенных сообщения показывают состояние задачи.
static void Main() { var tokenSource = new CancellationTokenSource(); var token = tokenSource.Token; var task = new Task(() => DoLongRunningTask(token), token); Console.WriteLine("Press Enter to cancel"); task.Start(); Console.ReadLine(); tokenSource.Cancel(); task.Wait(); Console.WriteLine("Status: {0}", task.Status); Console.WriteLine("IsCanceled: {0}", task.IsCanceled); Console.WriteLine("IsCompleted: {0}", task.IsCompleted); task.Dispose(); Console.ReadLine(); } / * КОНЕЧНЫЙ РЕЗУЛЬТАТ Status: RanToCompletion IsCanceled: False IsCompleted: True */OperationCanceledException
Чтобы отменить задачу и правильно установить свойства состояния, вы можете создать исключение OperationCanceledException. В отличие от других сценариев, где выбрасывание исключений не рекомендуется для управления нормальным потоком программы, этот поддерживаемый способ указывает на отмену задачи. Вместо того, чтобы проверить имущество IsCancellationRequested и бросать исключение вручную, вы можете использовать метод ThrowIfCancellationRequested маркера. Как следует из названия, это создает исключение только в том случае, если требуется отмена.
Чтобы правильно использовать исключение для отмены задачи, измените метод DoLongRunningTask следующим образом:
static void DoLongRunningTask(CancellationToken token) { token.ThrowIfCancellationRequested(); for (int i = 0; i <= 100; i++) { Console.WriteLine("{0}%", i); Thread.Sleep(1000); token.ThrowIfCancellationRequested(); } }Когда задача отменяется, вызывая исключение, мы должны захватить это исключение и справиться с ним соответствующим образом. На данный момент добавьте блок try/catch вокруг вызова Wait, как показано ниже. Запустите код, чтобы убедиться, что статусы теперь верны.
Примечание: если вы забудете передать токен конструктору задачи, создание исключения установит статус Faulted, а не Cancelled.
static void Main() { var tokenSource = new CancellationTokenSource(); var token = tokenSource.Token; var task = new Task(() => DoLongRunningTask(token), token); Console.WriteLine("Press Enter to cancel"); task.Start(); Console.ReadLine(); tokenSource.Cancel(); try { task.Wait(); } catch { } Console.WriteLine("Status: {0}", task.Status); Console.WriteLine("IsCanceled: {0}", task.IsCanceled); Console.WriteLine("IsCompleted: {0}", task.IsCompleted); task.Dispose(); Console.ReadLine(); } / * КОНЕЧНЫЙ РЕЗУЛЬТАТ Status: Отменено IsCanceled: Правда IsCompleted: True */Обработка исключений
Последняя проблема с кодом заключается в том, что любое реальное исключение теперь будет проглощено пустым блоком catch. Мы знаем, что исключение, которое происходит внутри задачи, будет обернуто в AggregateException. Потенциально может быть несколько внутренних исключений, некоторые из которых являются OperationCancelledExceptions, которые следует игнорировать, но некоторые из них являются другими типами, которые должны быть обработаны или переосмыслены.
Мы можем быстро устранить OperationCanceledExceptions, используя метод дескриптора AggregateException. Этот удобный метод выполняет делегат Func для каждого из внутренних исключений. Если делегат возвращает true для отдельного исключения, то это исключение считается обработанным. Если все исключения помечены как обработанные, обработка продолжается в обычном режиме. Однако если какое-либо из исключений приводит к ложному результату, они добавляются в новое исключение AggregateException и перестраиваются. Мы можем использовать это для обработки всех OperationCancelledExceptions, просто проверив их типы. Любое исключение другого типа будет переосмыслено и может быть обработано отдельно.
Чтобы игнорировать исключения отмены, измените блок try / catch следующим образом. Обратите внимание, что лямбда-выражение просто проверяет тип с помощью ключевого слова "is". После внесения изменений запустите программу еще раз, чтобы убедиться, что она работает правильно.
try { task.Wait(); } catch(AggregateException ex) { ex.Handle(e => { return e is OperationCanceledException; }); }Отмена нескольких задач
Во многих ситуациях вы будете запускать несколько параллельных задач. Когда требуется отмена, вы можете отменить группу задач, а не только одну. Это достигается за счет использования одного и того же маркера для каждой задачи в группе. Мы можем видеть это в следующем примере. Здесь запускаются две задачи и обе отменяются одним вызовом метода отмены источника токена
static void Main() { var tokenSource = new CancellationTokenSource(); var token = tokenSource.Token; var task = new Task(() => DoLongRunningTask(token), token); var task2 = new Task(() => DoLongRunningTask(token), token); Console.WriteLine("Press Enter to cancel"); task.Start(); task2.Start(); Console.ReadLine(); tokenSource.Cancel(); try { Task.WaitAll(task, task2); } catch (AggregateException ex) { ex.Handle(e => { return e is OperationCanceledException; }); } task.Dispose(); task2.Dispose(); Console.ReadLine(); }Примечание: если у вас есть несколько групп задач, которые могут быть отменены, используйте отдельный CancellationTokenSource для генерации токенов для каждой группы.
Автор этого материала - я - Пахолков Юрий. Я оказываю услуги по написанию программ на языках Java, C++, C# (а также консультирую по ним) и созданию сайтов. Работаю с сайтами на CMS OpenCart, WordPress, ModX и самописными. Кроме этого, работаю напрямую с JavaScript, PHP, CSS, HTML - то есть могу доработать ваш сайт или помочь с веб-программированием. Пишите сюда.
Читайте также:
Отправляя сообщение я подтверждаю, что ознакомлен и согласен с политикой конфиденциальности данного сайта.