Руководство программиста .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
для запуска процессов, а не для их остановки.
Рассмотрим следующее:
- Создайте класс
RandomWorker
. -
RandomWorker
должен иметь методDoWorkAsync
, выполняющий некоторую случайную работу. - Метод
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.
Дальнейшее чтение в блоге Toptal Engineering:
- .NET Core: безумие и открытый исходный код. Майкрософт, что так долго?!
- Создание веб-API ASP.NET с помощью ASP.NET Core
- Как загружать и создавать проекты .NET