Руководство программиста .NET по CancellationToken

Опубликовано: 2022-08-23

Иногда отмена — это хорошо. Во многих моих проектах .NET у меня было достаточно мотивации для отмены как внутренних, так и внешних процессов. Microsoft узнала, что разработчики подходят к этому распространенному варианту использования в различных сложных реализациях, и решила, что должен быть лучший способ. Таким образом, был представлен общий шаблон связи отмены как CancellationToken , который был построен с использованием низкоуровневых многопоточных конструкций и конструкций межпроцессного взаимодействия. В рамках моего первоначального исследования этого шаблона — и после того, как я изучил фактический исходный код .NET для реализации Microsoft — я обнаружил, что CancellationToken может решить гораздо более широкий набор проблем: подписки на состояния выполнения приложений, тайм-аут операций с использованием различных триггеры и общее межпроцессное взаимодействие через флаги.

Предполагаемый вариант использования CancellationToken

CancellationToken был представлен в .NET 4 как средство улучшения и стандартизации существующих решений для отмены операций. Существует четыре основных подхода к отмене, которые обычно реализуются в популярных языках программирования:

Убийство Скажи, не принимай за ответ Вежливо спросите и примите отказ Вежливо установите флаг, пусть голосует, если хочет
Подход Жесткая остановка; устранить несоответствия позже Скажи ему остановиться, но пусть он все исправит Прямая, но нежная просьба остановиться Попросите его остановиться, но не заставляйте его
Резюме Верный путь к коррупции и боли Разрешает чистые точки остановки, но он должен останавливаться Разрешает чистые точки остановки, но запрос на отмену может быть проигнорирован Отмена запрашивается через флаг
Потоки pthread_kill ,
pthread_cancel (асинхронный)
pthread_cancel (отложенный режим) н/д Через флаг
.СЕТЬ Thread.Abort н/д Thread.Interrupt Через флаг в CancellationToken
Ява Thread.destroy ,
Thread.stop
н/д Thread.interrupt Через флаг или Thread.interrupted
питон PyThreadState_SetAsyncExc н/д asyncio.Task.cancel Через флаг
Руководство Неприемлемо; избегайте этого подхода Приемлемо, особенно если язык не поддерживает исключения или раскрутку. Приемлемо, если язык поддерживает это Лучше, но больше групповой работы
Краткое изложение подхода к отмене и языковые примеры

CancellationToken находится в последней категории, где диалог об отмене является совместным.

После того, как Microsoft представила CancellationToken , сообщество разработчиков быстро приняло его, особенно потому, что многие основные API .NET были обновлены для использования этих токенов изначально. Например, начиная с ASP.NET Core 2.0, действия поддерживают необязательный параметр CancellationToken , который может сигнализировать о закрытии HTTP-запроса, что позволяет отменить любую операцию и, таким образом, избежать ненужного использования ресурсов.

После глубокого погружения в кодовую базу .NET стало ясно, что использование CancellationToken не ограничивается отменой.

CancellationToken под микроскопом

При более внимательном рассмотрении реализации CancellationToken мы видим, что это всего лишь простой флаг (то есть ManualResetEvent ) и вспомогательная инфраструктура, обеспечивающая возможность мониторинга и изменения этого флага. Основная полезность CancellationToken заключена в его названии, которое предполагает, что это распространенный способ отмены операций. В настоящее время любая библиотека, пакет или платформа .NET с асинхронными или длительными операциями допускает отмену с помощью этих токенов.

CancellationToken можно активировать либо вручную установив для его флага значение «true», либо запрограммировав его на изменение на «true» по истечении определенного промежутка времени. Независимо от того, как запускается CancellationToken , клиентский код, отслеживающий этот токен, может определить значение флага токена одним из трех способов:

  • Использование WaitHandle
  • Опрос флага CancellationToken
  • Информирование клиентского кода при обновлении состояния флага через программную подписку

После дальнейшего изучения кодовой базы .NET стало очевидно, что команда .NET нашла CancellationTokens полезными в других сценариях, не связанных с отменой. Давайте рассмотрим некоторые из этих расширенных и нестандартных вариантов использования, которые позволяют разработчикам C# использовать многопоточность и координацию между процессами для упрощения сложных ситуаций.

CancellationTokens для расширенных событий

При написании приложений ASP.NET Core нам иногда нужно знать, когда наше приложение запустилось, или нам нужно внедрить наш код в процесс завершения работы хоста. В этих случаях мы используем интерфейс IHostApplicationLifetime (ранее IApplicationLifetime ). Этот интерфейс (из репозитория .NET Core) использует CancellationToken для передачи трех основных событий: ApplicationStarted , ApplicationStopping и ApplicationStopped :

 namespace Microsoft.Extensions.Hosting { /// <summary> /// Allows consumers to be notified of application lifetime events. /// This interface is not intended to be user-replaceable. /// </summary> public interface IHostApplicationLifetime { /// <summary> /// Triggered when the application host has fully started. /// </summary> CancellationToken ApplicationStarted { get; } /// <summary> /// Triggered when the application host is starting a graceful shutdown. /// Shutdown will block until all callbacks registered on /// this token have completed. /// </summary> CancellationToken ApplicationStopping { get; } /// <summary> /// Triggered when the application host has completed a graceful shutdown. /// The application will not exit until all callbacks registered on /// this token have completed. /// </summary> CancellationToken ApplicationStopped { get; } /// <summary> /// Requests termination of the current application. /// </summary> void StopApplication(); } }

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

  • Они являются гибкими, позволяя клиенту интерфейса прослушивать эти события несколькими способами.
  • Они потокобезопасны из коробки.
  • Их можно создавать из разных источников, комбинируя CancellationToken s.

Хотя CancellationToken не идеально подходит для каждого события, оно идеально подходит для событий, которые происходят только один раз, например запуск или остановка приложения.

CancellationToken для тайм-аута

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

IHostedService StopAsync — отличный пример такого использования:

 namespace Microsoft.Extensions.Hosting { /// <summary> /// Defines methods for objects that are managed by the host. /// </summary> public interface IHostedService { /// <summary> /// Triggered when the application host is ready to start the service. /// </summary> /// <param name="cancellationToken">Indicates that the start /// process has been aborted.</param> Task StartAsync(CancellationToken cancellationToken); /// <summary> /// Triggered when the application host is performing a graceful shutdown. /// </summary> /// <param name="cancellationToken">Indicates that the shutdown /// process should no longer be graceful.</param> Task StopAsync(CancellationToken cancellationToken); } }

Как видно из определения интерфейса IHostedService , метод StopAsync принимает один параметр CancellationToken . Комментарий, связанный с этим параметром, ясно сообщает, что первоначальным намерением Microsoft для CancellationToken был механизм тайм-аута, а не процесс отмены.

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

На данный момент давайте забудем, что мы знаем, как устроен метод StopAsync , и вместо этого подумаем о том, как бы мы спроектировали контракт этого метода. Сначала определим требования:

  • Метод StopAsync должен попытаться остановить службу.
  • Метод StopAsync должен иметь корректное состояние остановки.
  • Независимо от того, достигнуто ли состояние изящной остановки, размещенная служба должна иметь максимальное время для остановки, как определено нашим параметром тайм-аута.

Имея метод StopAsync в любой форме, мы удовлетворяем первому требованию. Остальные требования сложны. CancellationToken в точности удовлетворяет этим требованиям, используя стандартный инструмент связи на основе флагов .NET для расширения диалога.

CancellationToken как механизм уведомления

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

Рассмотрим следующее:

  1. Создайте класс RandomWorker .
  2. RandomWorker должен иметь метод DoWorkAsync , выполняющий некоторую случайную работу.
  3. Метод DoWorkAsync должен позволять вызывающей стороне указывать, когда должна начаться работа.
 public class RandomWorker { public RandomWorker(int id) { Id = id; } public int Id { get; } public async Task DoWorkAsync() { for (int i = 1; i <= 10; i++) { Console.WriteLine($"[Worker {Id}] Iteration {i}"); await Task.Delay(1000); } } }

Приведенный выше класс удовлетворяет первым двум требованиям, оставляя нам третье. Есть несколько альтернативных интерфейсов, которые мы могли бы использовать для запуска нашего воркера, например временной интервал или простой флаг:

 # With a time span Task DoWorkAsync(TimeSpan startAfter); # Or a simple flag bool ShouldStart { get; set; } Task DoWorkAsync();

Эти два подхода хороши, но нет ничего более элегантного, чем использование CancellationToken :

 public class RandomWorker { public RandomWorker(int id) { Id = id; } public int Id { get; } public async Task DoWorkAsync(CancellationToken startToken) { startToken.WaitHandle.WaitOne(); for (int i = 1; i <= 10; i++) { Console.WriteLine($"[Worker {Id}] Iteration {i}"); await Task.Delay(1000); } } }

Этот пример клиентского кода иллюстрирует мощь этого дизайна:

 using System; using System.Linq; using System.Threading; using System.Threading.Tasks; namespace CancelToStart { public class Program { static void Main(string[] args) { CancellationTokenSource startCts = new CancellationTokenSource(); startCts.CancelAfter(TimeSpan.FromSeconds(10)); var tasks = Enumerable.Range(0, 10) .Select(i => new RandomWorker(i)) .Select(worker => worker.DoWorkAsync(startCts.Token)) .ToArray(); Task.WaitAll(tasks, CancellationToken.None); } } }

CancellationTokenSource создаст наш CancellationToken за кулисами и координирует запуск всех связанных процессов. В данном случае связанный процесс — это наш RandomWorker , который ожидает запуска. Этот подход позволяет нам использовать безопасность потоков, встроенную в реализацию CancellationToken по умолчанию.

Расширенный набор инструментов CancellationToken

Эти примеры демонстрируют, как CancellationToken предоставляет набор инструментов для решений, полезных за пределами предполагаемого варианта использования. Эти инструменты могут пригодиться во многих сценариях, связанных с обменом данными между процессами на основе флагов. Независимо от того, сталкиваемся ли мы с тайм-аутами, уведомлениями или разовыми событиями, мы можем прибегнуть к этой элегантной реализации, протестированной Microsoft.

Сверху вниз появляются слова «Gold» (золотого цвета), «Microsoft» и «Partner» (оба черного цвета), за которыми следует логотип Microsoft.
Будучи золотым партнером Microsoft, Toptal — это ваша элитная сеть экспертов Microsoft. Создавайте высокоэффективные команды с нужными вам экспертами — в любом месте и именно тогда, когда они вам нужны!

Дальнейшее чтение в блоге Toptal Engineering:

  • .NET Core: безумие и открытый исходный код. Майкрософт, что так долго?!
  • Создание веб-API ASP.NET с помощью ASP.NET Core
  • Как загружать и создавать проекты .NET