День 1986. #ЗаметкиНаПолях #Cancellation
Отмена. Часть 1: Обзор. Начало
Отмена — это тема, о которой возникает множество вопросов. И хотя документация Microsoft довольно хороша, в этой серии подробно рассмотрим эту тему.
Скоординированная отмена в .NET
Это означает, что одна часть кода запрашивает отмену, а другая часть кода отвечает на этот запрос. Мы часто говорим о том, что один код «отменяет» другой код, но на самом деле запрашивающий код просто вежливо уведомляет другой код о том, что он хотел бы его остановить, а отвечающий код может отреагировать на этот запрос на отмену любым способом. Отвечающий код может немедленно остановить то, что он делает, или продолжать работу до тех пор, пока не достигнет допустимой точки остановки, либо он может полностью игнорировать запрос на отмену.
Важный вывод: отвечающий код должен ответить на запрос отмены, чтобы отмена действительно что-то отменила.
Обычно возникает вопрос: как отменить неотменяемый код? Это сложный сценарий, рассмотрим его позже.
Токены отмены и «90% случаев»
В .NET токен отмены является «носителем» запроса на отмену. Запрашивающий код вызывает отмену на токене, а отвечающий код реагирует на это. Т.е. токен отмены — это то, как запрос отмены передается от запрашивающего кода к отвечающему коду.
Поэтому в 90% случаев добавьте параметр CancellationToken в метод, а затем передайте его любому API, который вы вызываете:
Токен отмены может обозначать отмену любого типа: нажатие пользователем кнопки «Отмена»; клиент отключается от сервера; закрытие приложения; тайм-аут. Для вашего кода не должно иметь значения, почему он отменяется; просто его отменяют.
Каждый токен можно отменить только один раз; как только токен отменён, он отменён навсегда.
Контракт отмены: сигнатура метода
По соглашению параметр CancellationToken обычно является последним, если не присутствует параметр IProgress<T>. Обычно предоставляется перегрузка или значение по умолчанию, чтобы вызывающему коду не приходилось предоставлять CancellationToken, если его нет; значение CancellationToken по умолчанию - CancellationToken.None, т.е. токен, который никогда не будет отменён:
Некоторые сигнатуры методов принимают CancellationToken и значение тайм-аута как отдельные параметры. В основном это делается в BCL, чтобы обеспечить более эффективные методы p/Invoke, которые принимают параметры тайм-аута. Если не используете API p/Invoke, используйте только CancellationToken, который может представлять собой любой вид отмены.
Принимая параметр CancellationToken, метод неявно заявляет, что он может отреагировать на отмену. Технически это означает «может ответить», а не «должен ответить». В некоторых случаях (например, при реализации интерфейса) аргумент CancellationToken может игнорироваться. Таким образом, наличие параметра CancellationToken не обязательно означает, что код должен поддерживать отмену.
Окончание следует…
Источник: https://blog.stephencleary.com/2022/02/cancellation-1-overview.html
Отмена. Часть 1: Обзор. Начало
Отмена — это тема, о которой возникает множество вопросов. И хотя документация Microsoft довольно хороша, в этой серии подробно рассмотрим эту тему.
Скоординированная отмена в .NET
Это означает, что одна часть кода запрашивает отмену, а другая часть кода отвечает на этот запрос. Мы часто говорим о том, что один код «отменяет» другой код, но на самом деле запрашивающий код просто вежливо уведомляет другой код о том, что он хотел бы его остановить, а отвечающий код может отреагировать на этот запрос на отмену любым способом. Отвечающий код может немедленно остановить то, что он делает, или продолжать работу до тех пор, пока не достигнет допустимой точки остановки, либо он может полностью игнорировать запрос на отмену.
Важный вывод: отвечающий код должен ответить на запрос отмены, чтобы отмена действительно что-то отменила.
Обычно возникает вопрос: как отменить неотменяемый код? Это сложный сценарий, рассмотрим его позже.
Токены отмены и «90% случаев»
В .NET токен отмены является «носителем» запроса на отмену. Запрашивающий код вызывает отмену на токене, а отвечающий код реагирует на это. Т.е. токен отмены — это то, как запрос отмены передается от запрашивающего кода к отвечающему коду.
Поэтому в 90% случаев добавьте параметр CancellationToken в метод, а затем передайте его любому API, который вы вызываете:
async Task DoAsync(
int data,
CancellationToken ct)
{
var myVal = await DoFirstAsync(data, ct);
await DoSecondAsync(myVal, ct);
}
Токен отмены может обозначать отмену любого типа: нажатие пользователем кнопки «Отмена»; клиент отключается от сервера; закрытие приложения; тайм-аут. Для вашего кода не должно иметь значения, почему он отменяется; просто его отменяют.
Каждый токен можно отменить только один раз; как только токен отменён, он отменён навсегда.
Контракт отмены: сигнатура метода
По соглашению параметр CancellationToken обычно является последним, если не присутствует параметр IProgress<T>. Обычно предоставляется перегрузка или значение по умолчанию, чтобы вызывающему коду не приходилось предоставлять CancellationToken, если его нет; значение CancellationToken по умолчанию - CancellationToken.None, т.е. токен, который никогда не будет отменён:
async Task DoAsync(int data) =>
DoAsync(data, CancellationToken.None);
async Task DoAsync(int data, CancellationToken ct)
{
…
}
// либо
async Task DoAsync(
int data, CancellationToken ct = default)
{
…
}
Некоторые сигнатуры методов принимают CancellationToken и значение тайм-аута как отдельные параметры. В основном это делается в BCL, чтобы обеспечить более эффективные методы p/Invoke, которые принимают параметры тайм-аута. Если не используете API p/Invoke, используйте только CancellationToken, который может представлять собой любой вид отмены.
Принимая параметр CancellationToken, метод неявно заявляет, что он может отреагировать на отмену. Технически это означает «может ответить», а не «должен ответить». В некоторых случаях (например, при реализации интерфейса) аргумент CancellationToken может игнорироваться. Таким образом, наличие параметра CancellationToken не обязательно означает, что код должен поддерживать отмену.
Окончание следует…
Источник: https://blog.stephencleary.com/2022/02/cancellation-1-overview.html
👍20
День 1987. #ЗаметкиНаПолях #Cancellation
Отмена. Часть 1: Обзор. Окончание
Начало
Контракт отмены: ответ
Как мы уже говорили, когда запрашивается отмена, отвечающий код может отменить то, что он делает, а может и нет. Даже если он попытается отменить, обычно возникает состояние гонки, и метод может фактически завершиться до того, как запрос на отмену будет обработан. Контракт отмены решает эту проблему путем фиксирования факта отмены кода через выброс исключения OperationCanceledException, когда запрошена отмена и фактически отменяется некоторая работа. Если запрос на отмену игнорируется или поступает слишком поздно и работа таки завершается, метод возвращается в обычном режиме.
Стандартный код для «90% случаев» обрабатывает это неявно; если DoFirstAsync или DoSecondAsync выбрасывают OperationCanceledException, то это исключение также распространяется из DoAsync. Никаких изменений в коде в «90% случаев» не требуется:
Существует множество примеров кода, которые просто молча делают ранний возврат, когда запрашивается отмена. Пожалуйста не делайте так; это нарушение контракта отмены! Когда отвечающий код просто возвращается раньше, вызывающий код не может знать, был ли его запрос на отмену удовлетворён или проигнорирован.
Исключение из «90% случаев»
Код в «90% случаев» просто принимает параметр CancellationToken и передаёт его дальше. Из этого правила есть одно заметное исключение: не следует передавать токен отмены в Task.Run.
Причина в том, что семантика сбивает с толку. Многие разработчики передают делегат и токен отмены в Task.Run и ожидают, что делегат будет отменён при отмене токена, но этого не происходит. Токен отмены, передаваемый в Task.Run, просто отменяет планирование делегата в пуле потоков; как только этот делегат начинает работать (что происходит практически сразу), этот токен отмены игнорируется.
Вот что пишут многие разработчики, ошибочно ожидая, что
Никогда не передавая CancellationToken в Task.Run (который в любом случае игнорируется, если только не возникает серьёзная конкуренция за пул потоков или токен уже не отменен), мы яснее даём понять, что сам делегат должен реагировать на токен:
Источник: https://blog.stephencleary.com/2022/02/cancellation-1-overview.html
Отмена. Часть 1: Обзор. Окончание
Начало
Контракт отмены: ответ
Как мы уже говорили, когда запрашивается отмена, отвечающий код может отменить то, что он делает, а может и нет. Даже если он попытается отменить, обычно возникает состояние гонки, и метод может фактически завершиться до того, как запрос на отмену будет обработан. Контракт отмены решает эту проблему путем фиксирования факта отмены кода через выброс исключения OperationCanceledException, когда запрошена отмена и фактически отменяется некоторая работа. Если запрос на отмену игнорируется или поступает слишком поздно и работа таки завершается, метод возвращается в обычном режиме.
Стандартный код для «90% случаев» обрабатывает это неявно; если DoFirstAsync или DoSecondAsync выбрасывают OperationCanceledException, то это исключение также распространяется из DoAsync. Никаких изменений в коде в «90% случаев» не требуется:
async Task DoAsync(
int data,
CancellationToken ct)
{
var myVal = await DoFirstAsync(data, ct);
await DoSecondAsync(myVal, ct);
}
Существует множество примеров кода, которые просто молча делают ранний возврат, когда запрашивается отмена. Пожалуйста не делайте так; это нарушение контракта отмены! Когда отвечающий код просто возвращается раньше, вызывающий код не может знать, был ли его запрос на отмену удовлетворён или проигнорирован.
Исключение из «90% случаев»
Код в «90% случаев» просто принимает параметр CancellationToken и передаёт его дальше. Из этого правила есть одно заметное исключение: не следует передавать токен отмены в Task.Run.
Причина в том, что семантика сбивает с толку. Многие разработчики передают делегат и токен отмены в Task.Run и ожидают, что делегат будет отменён при отмене токена, но этого не происходит. Токен отмены, передаваемый в Task.Run, просто отменяет планирование делегата в пуле потоков; как только этот делегат начинает работать (что происходит практически сразу), этот токен отмены игнорируется.
Вот что пишут многие разработчики, ошибочно ожидая, что
// Что-то делаем будет отменено после его запуска:async Task DoSomethingAsync(CancellationToken ct)
{
await Task.Run(() =>
{
// Что-то делаем
}, ct);
…
}
Никогда не передавая CancellationToken в Task.Run (который в любом случае игнорируется, если только не возникает серьёзная конкуренция за пул потоков или токен уже не отменен), мы яснее даём понять, что сам делегат должен реагировать на токен:
async Task DoSomethingAsync(CancellationToken ct)
{
await Task.Run(() =>
{
// Что-то делаем
// IDE сообщает, что ct не используется,
// поэтому делегату нужно его использовать.
});
…
}
Источник: https://blog.stephencleary.com/2022/02/cancellation-1-overview.html
👍20
День 1993. #ЗаметкиНаПолях #Cancellation
Отмена. Часть 2: Запрос Отмены.
В прошлый раз мы рассмотрели базовый контракт отмены. Теперь рассмотрим, как создавать токены отмены для запроса отмены.
CancellationTokenSource
Некоторые токены отмены предоставляются используемой вами платформой или библиотекой. Например, ASP.NET предоставит вам CancellationToken, который представляет собой неожиданное отключение клиента. Polly может предоставить вашему делегату токен, который представляет собой более общую отмену (например, срабатывание политики тайм-аута).
В других сценариях вам потребуется предоставить собственный токен. В общем случае, когда вы хотите создать CancellationToken, который можно будет отменить позже, нужно использовать CancellationTokenSource.
Каждый CancellationTokenSource управляет своим набором токенов. Каждый токен, созданный из CancellationTokenSource, представляет собой небольшую структуру, которая ссылается на свой CancellationTokenSource. Токен отмены может только отвечать на запросы отмены; CancellationTokenSource необходим для запроса отмены. Поэтому запрашивающий код создает CancellationTokenSource и сохраняет ссылку на него (используя эту ссылку позже для запроса отмены), а отвечающий код просто получает CancellationToken и использует его для ответа на запросы отмены.
Таймауты
Одной из распространённых потребностей отмены является реализация тайм-аута. Решение состоит в том, чтобы иметь таймер, который запрашивает отмену по истечении срока его действия. В CancellationTokenSource такое поведение встроено. Вы можете использовать конструктор CancellationTokenSource, который принимает таймаут, или вызвать CancelAfter для существующего CancellationTokenSource:
Очистка
Чтобы избежать утечек ресурсов, важно уничтожать (dispose) экземпляры CancellationTokenSource. Очищается несколько видов ресурсов: во-первых, таймер тайм-аута (если он есть); во-вторых, все «слушатели», прикрепленные к токенам отмены. Эта очистка выполняется при отмене или уничтожении CancellationTokenSource. Вы должны убедиться, что либо одно, либо другое выполнено, чтобы избежать утечек ресурсов.
Если токен отмены сохраняется и используется позже, вы бы не хотели уничтожать CancellationTokenSource. В этом случае следует сохранять CancellationTokenSource активным до тех пор, пока вы не будете уверены, что весь код, использующий его токены, выполнен. Это более сложный случай, и иногда удобнее отменить CancellationTokenSource, а не уничтожать его.
Предупреждение: уничтожение связанных источников CancellationTokenSource.
Вызов Cancel или Dispose очистит все регистрации для конкретного CancellationTokenSource. Однако если этот CancellationTokenSource связан с «родительским» CancellationTokenSource, вызов Cancel для «дочернего» CancellationTokenSource приведёт к отмене регистрации в «родительском» CancellationTokenSource. Вы можете вызвать Dispose (или использовать оператор using) для «дочернего элемента», чтобы очистить регистрацию у родителя этого дочернего элемента, разрывая ссылку.
Эта и другие темы, касающиеся связанных CancellationTokenSource, будут рассмотрены далее в этой серии.
Источник: https://blog.stephencleary.com/2022/03/cancellation-2-requesting-cancellation.html
Отмена. Часть 2: Запрос Отмены.
В прошлый раз мы рассмотрели базовый контракт отмены. Теперь рассмотрим, как создавать токены отмены для запроса отмены.
CancellationTokenSource
Некоторые токены отмены предоставляются используемой вами платформой или библиотекой. Например, ASP.NET предоставит вам CancellationToken, который представляет собой неожиданное отключение клиента. Polly может предоставить вашему делегату токен, который представляет собой более общую отмену (например, срабатывание политики тайм-аута).
В других сценариях вам потребуется предоставить собственный токен. В общем случае, когда вы хотите создать CancellationToken, который можно будет отменить позже, нужно использовать CancellationTokenSource.
Каждый CancellationTokenSource управляет своим набором токенов. Каждый токен, созданный из CancellationTokenSource, представляет собой небольшую структуру, которая ссылается на свой CancellationTokenSource. Токен отмены может только отвечать на запросы отмены; CancellationTokenSource необходим для запроса отмены. Поэтому запрашивающий код создает CancellationTokenSource и сохраняет ссылку на него (используя эту ссылку позже для запроса отмены), а отвечающий код просто получает CancellationToken и использует его для ответа на запросы отмены.
Таймауты
Одной из распространённых потребностей отмены является реализация тайм-аута. Решение состоит в том, чтобы иметь таймер, который запрашивает отмену по истечении срока его действия. В CancellationTokenSource такое поведение встроено. Вы можете использовать конструктор CancellationTokenSource, который принимает таймаут, или вызвать CancelAfter для существующего CancellationTokenSource:
async Task DoSomethingWithTimeoutAsync()
{
// Создаём CTS, который отменится через 5 минут
using CancellationTokenSource cts
= new(TimeSpan.FromMinutes(5));
// Передаём токен
await DoSomethingAsync(cts.Token);
// В конце метода CTS уничтожается
// Его токены не могут больше использоваться
}
Очистка
Чтобы избежать утечек ресурсов, важно уничтожать (dispose) экземпляры CancellationTokenSource. Очищается несколько видов ресурсов: во-первых, таймер тайм-аута (если он есть); во-вторых, все «слушатели», прикрепленные к токенам отмены. Эта очистка выполняется при отмене или уничтожении CancellationTokenSource. Вы должны убедиться, что либо одно, либо другое выполнено, чтобы избежать утечек ресурсов.
Если токен отмены сохраняется и используется позже, вы бы не хотели уничтожать CancellationTokenSource. В этом случае следует сохранять CancellationTokenSource активным до тех пор, пока вы не будете уверены, что весь код, использующий его токены, выполнен. Это более сложный случай, и иногда удобнее отменить CancellationTokenSource, а не уничтожать его.
Предупреждение: уничтожение связанных источников CancellationTokenSource.
Вызов Cancel или Dispose очистит все регистрации для конкретного CancellationTokenSource. Однако если этот CancellationTokenSource связан с «родительским» CancellationTokenSource, вызов Cancel для «дочернего» CancellationTokenSource приведёт к отмене регистрации в «родительском» CancellationTokenSource. Вы можете вызвать Dispose (или использовать оператор using) для «дочернего элемента», чтобы очистить регистрацию у родителя этого дочернего элемента, разрывая ссылку.
Эта и другие темы, касающиеся связанных CancellationTokenSource, будут рассмотрены далее в этой серии.
Источник: https://blog.stephencleary.com/2022/03/cancellation-2-requesting-cancellation.html
👍16
День 2001. #ЗаметкиНаПолях #Cancellation
Отмена. Часть 3: Обнаружение Отмены. Начало
Нередко возникает желание определить, действительно ли запрос на отмену что-то отменил. Отмена – это скоординированная операция, и иногда коду, запрашивающему отмену, необходимо знать, действительно ли отмена имела место или операция просто завершилась нормально.
В контракте отмены есть способ сообщить об этом: методы, принимающие CancellationToken, по соглашению должны выдавать исключение OperationCanceledException при их отмене. Это верно для всех методов BCL и должно быть так и в вашем коде.
Ответ на отмену
Самый распространённый сценарий обнаружения отмены — не обрабатывать исключение при отмене. Обычно исключение OperationCanceledException просто игнорируется:
Код выше обработает непредвиденные ошибки, но проигнорирует исключения отмены.
Если код должен сделать что-то другое при отмене, можно обработать это в блоке catch:
Однако подумайте, действительно ли вам нужно это делать, потому что это редкий случай, вызывает вопросы по логике кода, а также такой код сложно тестировать.
Окончание следует…
Источник: https://blog.stephencleary.com/2022/03/cancellation-3-detecting-cancellation.html
Отмена. Часть 3: Обнаружение Отмены. Начало
Нередко возникает желание определить, действительно ли запрос на отмену что-то отменил. Отмена – это скоординированная операция, и иногда коду, запрашивающему отмену, необходимо знать, действительно ли отмена имела место или операция просто завершилась нормально.
В контракте отмены есть способ сообщить об этом: методы, принимающие CancellationToken, по соглашению должны выдавать исключение OperationCanceledException при их отмене. Это верно для всех методов BCL и должно быть так и в вашем коде.
Ответ на отмену
Самый распространённый сценарий обнаружения отмены — не обрабатывать исключение при отмене. Обычно исключение OperationCanceledException просто игнорируется:
async Task TryDoSomethingAsync()
{
using CancellationTokenSource cts = new();
// Создаём что-то, что может отменять cts
try
{
await DoAsync(cts.Token);
}
catch (Exception ex) when
(ex is not OperationCanceledException)
{
// Обработка исключения
}
}
Код выше обработает непредвиденные ошибки, но проигнорирует исключения отмены.
Если код должен сделать что-то другое при отмене, можно обработать это в блоке catch:
async Task DoSomethingAsync()
{
using CancellationTokenSource cts = new();
// Создаём что-то, что может отменять cts
try
{
await DoAsync(cts.Token);
}
catch (OperationCanceledException)
{
// Обработка отмены
}
catch (Exception ex)
{
// Обработка исключения
}
}
Однако подумайте, действительно ли вам нужно это делать, потому что это редкий случай, вызывает вопросы по логике кода, а также такой код сложно тестировать.
Окончание следует…
Источник: https://blog.stephencleary.com/2022/03/cancellation-3-detecting-cancellation.html
👍8
День 2002. #ЗаметкиНаПолях #Cancellation
Отмена. Часть 3: Обнаружение Отмены. Окончание
Начало
TaskCanceledException
Существует ещё один тип исключения для отмены: TaskCanceledException. Он выбрасывается некоторыми API вместо OperationCanceledException.
Как правило, рекомендуется не использовать TaskCanceledException. Некоторые API просто вызывают OperationCanceledException, даже если они имеют дело с отменёнными задачами. А поскольку TaskCanceledException является производным от OperationCanceledException, код обработчика исключений отмены может просто перехватывать OperationCanceledException, игнорировать TaskCanceledException, и это будет работать везде.
Внимание: не перехватывайте TaskCanceledException. Вместо этого перехватывайте OperationCanceledException.
OperationCanceledException.CancellationToken
OperationCanceledException имеет свойство CancellationToken. Это токен, вызвавший отмену. Это в том случае, если он установлен. Не все API устанавливают это значение для выбрасываемых ими исключений.
Если вашему коду необходимо определить, отменяет ли операцию он сам или это делает какой-то другой код, у вас может возникнуть соблазн использовать это свойство. Но лучше игнорировать его. Когда используются связанные токены отмены (об этом далее в этой серии), вполне возможно, что токен в этом свойстве на самом деле не является основной причиной отмены:
В коде выше есть проблема: в зависимости от реализации DoAsync возможно, что отмена cts приведёт к тому, что DoAsync выдаст исключение OperationCanceledException, но токен, на который ссылается это исключение, будет отличаться от токена cts.
Если вам действительно необходимо выполнить специальную обработку случая, когда происходит отмена через конкретный токен, попробуйте что-то вроде этого:
Технически, здесь мы проверяем не «привёл ли мой токен к отмене операции», а скорее «произошла ли отмена и запрашивал ли мой токен отмену». Но в подавляющем большинстве случаев такой проверки достаточно.
Внимание: не используйте OperationCanceledException.CancellationToken. Он может работать не так, как вы ожидаете.
Источник: https://blog.stephencleary.com/2022/03/cancellation-3-detecting-cancellation.html
Отмена. Часть 3: Обнаружение Отмены. Окончание
Начало
TaskCanceledException
Существует ещё один тип исключения для отмены: TaskCanceledException. Он выбрасывается некоторыми API вместо OperationCanceledException.
Как правило, рекомендуется не использовать TaskCanceledException. Некоторые API просто вызывают OperationCanceledException, даже если они имеют дело с отменёнными задачами. А поскольку TaskCanceledException является производным от OperationCanceledException, код обработчика исключений отмены может просто перехватывать OperationCanceledException, игнорировать TaskCanceledException, и это будет работать везде.
Внимание: не перехватывайте TaskCanceledException. Вместо этого перехватывайте OperationCanceledException.
OperationCanceledException.CancellationToken
OperationCanceledException имеет свойство CancellationToken. Это токен, вызвавший отмену. Это в том случае, если он установлен. Не все API устанавливают это значение для выбрасываемых ими исключений.
Если вашему коду необходимо определить, отменяет ли операцию он сам или это делает какой-то другой код, у вас может возникнуть соблазн использовать это свойство. Но лучше игнорировать его. Когда используются связанные токены отмены (об этом далее в этой серии), вполне возможно, что токен в этом свойстве на самом деле не является основной причиной отмены:
async Task DoSomethingAsync()
{
// Плохой код, не делайте так
using CancellationTokenSource cts = new();
// Создаём что-то, что может отменять cts
try
{
await DoAsync(cts.Token);
}
catch (OperationCanceledException ex)
when (ex.CancellationToken == cts.Token)
{
// Обработка отмены «нашим» токеном
}
}
В коде выше есть проблема: в зависимости от реализации DoAsync возможно, что отмена cts приведёт к тому, что DoAsync выдаст исключение OperationCanceledException, но токен, на который ссылается это исключение, будет отличаться от токена cts.
Если вам действительно необходимо выполнить специальную обработку случая, когда происходит отмена через конкретный токен, попробуйте что-то вроде этого:
async Task DoSomethingAsync()
{
using CancellationTokenSource cts = new();
// Создаём что-то, что может отменять cts
try
{
await DoAsync(cts.Token);
}
catch (OperationCanceledException ex)
when (cts.IsCancellationRequested)
{
// Обработка отмены «нашим» токеном
}
}
Технически, здесь мы проверяем не «привёл ли мой токен к отмене операции», а скорее «произошла ли отмена и запрашивал ли мой токен отмену». Но в подавляющем большинстве случаев такой проверки достаточно.
Внимание: не используйте OperationCanceledException.CancellationToken. Он может работать не так, как вы ожидаете.
Источник: https://blog.stephencleary.com/2022/03/cancellation-3-detecting-cancellation.html
👍12
День 2008. #ЗаметкиНаПолях #Cancellation
Отмена. Часть 4: Проверка отмены
В большинстве случаев получаемый токен отмены просто передаётся на более низкий уровень. Но если нужен отменяемый код на самом низком уровне, есть несколько вариантов.
Обычный шаблон — периодически вызывать ThrowIfCancellationRequested:
В коде выше проверяется токен отмены до начала работы, что является хорошей практикой. Возможно, токен уже отменён к моменту начала выполнения вашей операции.
ThrowIfCancellationRequested проверит, запрошена ли отмена, и если да, то выдаст OperationCanceledException. Ваш код должен просто позволить этому исключению распространиться из метода.
Главный вопрос — как часто проверять. На него нет хорошего ответа; в идеале - несколько раз в секунду, но в общем случае - просто поместите его в лучшее место(а) и запустите несколько тестов, чтобы увидеть, достаточно ли отзывчива отмена.
Как не делать проверку
Существует антишаблон пулинга на предмет отмены в бесконечном цикле:
Код будет периодически проверять токен отмены; и при отмене происходит выход из метода. Этот метод не удовлетворяет контракту отмены, требующему выбрасывать исключение при отмене. Т.е. вызывающий код не может знать, был ли метод выполнен до конца или был отменён. Правильное решение — использовать ThrowIfCancellationRequested, даже для бесконечных циклов (как в предыдущем примере).
Когда проверять
Проверка - подходящий вариант для наблюдения за отменой, если код синхронный, например, код вычислительной операции. Но отмена в .NET применима как к синхронному коду, так и к асинхронному. Фактически, параллельные циклы и PLINQ имеют встроенную поддержку отмены: ParallelOptions.CancellationToken для Parallel и WithCancellation для PLINQ.
Уместно вводить ThrowIfCancellationRequested в асинхронный код, если вы не уверены, будут ли другие методы учитывать токен отмены. Помните, что метод, принимающий CancellationToken может учитывать отмену, но может и проигнорировать его. Поэтому в вашем коде можно добавить проверки отмены между «шагами»:
Хотя вы можете разбросать вызовы ThrowIfCancellationRequested по всему коду, как в примере выше, лучше это делать, только когда тестирование показывает, что код не учитывает отмену. Т.е. предполагайте, что DoStep1Async и DoStep2Async будут учитывать отмену, пока тестирование не докажет обратное.
Также уместно использовать ThrowIfCancellationRequested в определённых точках, где ваш код собирается сделать что-то затратное. Простое добавление туда проверки отмены означает, что вашему коду не придётся выполнять затратную работу, если он всё равно будет отменён.
Источник: https://blog.stephencleary.com/2022/03/cancellation-3-detecting-cancellation.html
Отмена. Часть 4: Проверка отмены
В большинстве случаев получаемый токен отмены просто передаётся на более низкий уровень. Но если нужен отменяемый код на самом низком уровне, есть несколько вариантов.
Обычный шаблон — периодически вызывать ThrowIfCancellationRequested:
void DoSomething(CancellationToken ct)
{
while (!done)
{
ct.ThrowIfCancellationRequested();
Thread.Sleep(200); // некоторая работа
}
}
В коде выше проверяется токен отмены до начала работы, что является хорошей практикой. Возможно, токен уже отменён к моменту начала выполнения вашей операции.
ThrowIfCancellationRequested проверит, запрошена ли отмена, и если да, то выдаст OperationCanceledException. Ваш код должен просто позволить этому исключению распространиться из метода.
Главный вопрос — как часто проверять. На него нет хорошего ответа; в идеале - несколько раз в секунду, но в общем случае - просто поместите его в лучшее место(а) и запустите несколько тестов, чтобы увидеть, достаточно ли отзывчива отмена.
Как не делать проверку
Существует антишаблон пулинга на предмет отмены в бесконечном цикле:
void DoSomethingForever(CancellationToken ct)
{
// плохой код, не делайте так
while (!ct.IsCancellationRequested)
Thread.Sleep(200); // некоторая работа
}
Код будет периодически проверять токен отмены; и при отмене происходит выход из метода. Этот метод не удовлетворяет контракту отмены, требующему выбрасывать исключение при отмене. Т.е. вызывающий код не может знать, был ли метод выполнен до конца или был отменён. Правильное решение — использовать ThrowIfCancellationRequested, даже для бесконечных циклов (как в предыдущем примере).
Когда проверять
Проверка - подходящий вариант для наблюдения за отменой, если код синхронный, например, код вычислительной операции. Но отмена в .NET применима как к синхронному коду, так и к асинхронному. Фактически, параллельные циклы и PLINQ имеют встроенную поддержку отмены: ParallelOptions.CancellationToken для Parallel и WithCancellation для PLINQ.
Уместно вводить ThrowIfCancellationRequested в асинхронный код, если вы не уверены, будут ли другие методы учитывать токен отмены. Помните, что метод, принимающий CancellationToken может учитывать отмену, но может и проигнорировать его. Поэтому в вашем коде можно добавить проверки отмены между «шагами»:
async Task DoComplexWorkAsync(CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
await DoStep1Async(ct);
ct.ThrowIfCancellationRequested();
await DoStep2Async(ct);
}
Хотя вы можете разбросать вызовы ThrowIfCancellationRequested по всему коду, как в примере выше, лучше это делать, только когда тестирование показывает, что код не учитывает отмену. Т.е. предполагайте, что DoStep1Async и DoStep2Async будут учитывать отмену, пока тестирование не докажет обратное.
Также уместно использовать ThrowIfCancellationRequested в определённых точках, где ваш код собирается сделать что-то затратное. Простое добавление туда проверки отмены означает, что вашему коду не придётся выполнять затратную работу, если он всё равно будет отменён.
Источник: https://blog.stephencleary.com/2022/03/cancellation-3-detecting-cancellation.html
👍29
День 2056. #ЗаметкиНаПолях #Cancellation
Отмена. Часть 5: Регистрация
Регистрация — это способ для вашего кода выполнить метод обратного вызова (callback) немедленно при запросе отмены. Этот метод может выполнить некоторую операцию (часто вызов другого API) для отмены асинхронной операции.
Внимание: из-за множества способов вызова обычно рекомендуется, чтобы callback-методы не выдавали исключения.
Как регистрировать
Ваш код может зарегистрировать callback-метод с любым токеном отмены, вызвав его метод Register. Callback-метод вызывается, когда (если) токен отменяется. Метод Register возвращает регистрацию токена отмены (структуру IDisposable).
Практически все асинхронные API в .NET поддержку отмены через токен, поэтому код ниже в качестве примера использует устаревший WebClient, который не принимает токена отмены:
Внимание: callback-метод может не быть вызван!
Задачи всегда должны завершаться, но некоторые токены отмены никогда не будут отменены, поэтому они никогда не вызовут свои callback-методы.
Состояние гонки
Что произойдёт, если токен отмены будет отменён примерно в то же время, когда зарегистрирован обратный вызов?
Если callback-метод добавляется к токену отмены, который уже отменён, то он немедленно и синхронно вызывается.
Важность очистки
При написании отменяемого кода убедитесь, что ваш код удаляет регистрацию; это предотвратит утечки ресурсов в приложении.
Внимание: синхронные вызовы callback-методов
Как обсуждалось в запросе отмены, источники токенов могут быть отменены путём вызова метода Cancel. Важно отметить, что любые зарегистрированные обратные вызовы немедленно (и синхронно) запускаются методом Cancel до того, как он вернёт управление. Это может стать источником взаимоблокировок или другого неожиданного поведения. Это неудобно, поэтому в .NET 8.0 был добавлен метод токена CancelAsync, который вызывает обратные вызовы отмены в потоке из пула.
Внимание: возможно, что CancelAsync вернётся до завершения обратных вызовов, если он уже был вызван.
Как только CancellationTokenSource перейдёт в отменённое состояние, любые последующие вызовы Cancel или CancelAsync вернутся немедленно, даже если предыдущий вызов CancelAsync ещё не завершил выполнение своих обратных вызовов.
Итого
Регистрация обратных вызовов — естественный способ реализации отмены на самых низких уровнях. Однако помните, что метод обратного вызова:
1. Не должен выдавать исключений.
2. Может никогда не быть вызван.
3. Может быть вызван немедленно в том же потоке до возврата из Register.
4. В большинстве случаев вызывается синхронно.
5. Регистрация обязательно должна удаляться.
Источник: https://blog.stephencleary.com/2024/08/cancellation-5-registration.html
Отмена. Часть 5: Регистрация
Регистрация — это способ для вашего кода выполнить метод обратного вызова (callback) немедленно при запросе отмены. Этот метод может выполнить некоторую операцию (часто вызов другого API) для отмены асинхронной операции.
Внимание: из-за множества способов вызова обычно рекомендуется, чтобы callback-методы не выдавали исключения.
Как регистрировать
Ваш код может зарегистрировать callback-метод с любым токеном отмены, вызвав его метод Register. Callback-метод вызывается, когда (если) токен отменяется. Метод Register возвращает регистрацию токена отмены (структуру IDisposable).
Практически все асинхронные API в .NET поддержку отмены через токен, поэтому код ниже в качестве примера использует устаревший WebClient, который не принимает токена отмены:
async Task DownloadAsync(CancellationToken ct)
{
using var wc = new WebClient();
using var ctr =
ct.Register(() => wc.CancelAsync());
await wc.DownloadStringTaskAsync(
new Uri("http://www.google.com"));
}
Внимание: callback-метод может не быть вызван!
Задачи всегда должны завершаться, но некоторые токены отмены никогда не будут отменены, поэтому они никогда не вызовут свои callback-методы.
Состояние гонки
Что произойдёт, если токен отмены будет отменён примерно в то же время, когда зарегистрирован обратный вызов?
Если callback-метод добавляется к токену отмены, который уже отменён, то он немедленно и синхронно вызывается.
Важность очистки
При написании отменяемого кода убедитесь, что ваш код удаляет регистрацию; это предотвратит утечки ресурсов в приложении.
using var registration в коде выше является одним из распространённых способов обработки очистки.Внимание: синхронные вызовы callback-методов
Как обсуждалось в запросе отмены, источники токенов могут быть отменены путём вызова метода Cancel. Важно отметить, что любые зарегистрированные обратные вызовы немедленно (и синхронно) запускаются методом Cancel до того, как он вернёт управление. Это может стать источником взаимоблокировок или другого неожиданного поведения. Это неудобно, поэтому в .NET 8.0 был добавлен метод токена CancelAsync, который вызывает обратные вызовы отмены в потоке из пула.
Внимание: возможно, что CancelAsync вернётся до завершения обратных вызовов, если он уже был вызван.
Как только CancellationTokenSource перейдёт в отменённое состояние, любые последующие вызовы Cancel или CancelAsync вернутся немедленно, даже если предыдущий вызов CancelAsync ещё не завершил выполнение своих обратных вызовов.
Итого
Регистрация обратных вызовов — естественный способ реализации отмены на самых низких уровнях. Однако помните, что метод обратного вызова:
1. Не должен выдавать исключений.
2. Может никогда не быть вызван.
3. Может быть вызван немедленно в том же потоке до возврата из Register.
4. В большинстве случаев вызывается синхронно.
5. Регистрация обязательно должна удаляться.
Источник: https://blog.stephencleary.com/2024/08/cancellation-5-registration.html
👍15
День 2122. #ЗаметкиНаПолях #Cancellation
Отмена. Часть 6: Связывание. Начало
До сих пор (см. предыдущие части по тегу #Cancellation) мы рассматривали, как отмена запрашивается одним фрагментом кода и на неё отвечает другой фрагмент кода. Запрашивающий код имеет стандартный способ запроса отмены, а также стандартный способ определения того, был код отменён или нет. Между тем, отвечающий код может наблюдать отмену либо путём опроса, либо путём регистрации обратного вызова отмены.
Связанные токены отмены
Связанные токены отмены позволяют вашему коду создавать связанный CancellationTokenSource, который отменяется другим токеном отмены, в дополнение к тому, что сам может запросить отмену:
Метод DoAsync принимает токен ct — «внешний» токен отмены. Затем он создает CTS, который связан с этим внешним токеном. Когда он вызывает DoOtherAsync, он передает «внутренний» токен из этого связанного CTS.
Если внешний токен когда-либо отменяется, то связанный cts и его внутренний токен (cts.Token) также отменяются. Более того, метод DoAsync имеет возможность явно отменить связанный CTS — в этом случае будет отменён только внутренний токен отмены, оставив внешний отмены неизменным.
То же самое можно сделать с помощью регистраций:
Действительно, логически это примерно то же самое: вы можете думать о связанном источнике токена отмены как о совершенно обычном источнике токена отмены вместе с регистрацией, которая отменяет его, когда отменяется какой-либо другой токен.
Множественные связи
Внутренний токен отмены выше отменяется, когда отменяется внешний токен или когда его источник явно его отменяет. Аналогично, мы можем передать любое количество токенов отмены в CreateLinkedTokenSource, и токен отмены, который он возвращает, будет отменён, когда отменится любой из внешних токенов.
Продолжение следует…
Источник: https://blog.stephencleary.com/2024/10/cancellation-6-linking.html
Отмена. Часть 6: Связывание. Начало
До сих пор (см. предыдущие части по тегу #Cancellation) мы рассматривали, как отмена запрашивается одним фрагментом кода и на неё отвечает другой фрагмент кода. Запрашивающий код имеет стандартный способ запроса отмены, а также стандартный способ определения того, был код отменён или нет. Между тем, отвечающий код может наблюдать отмену либо путём опроса, либо путём регистрации обратного вызова отмены.
Связанные токены отмены
Связанные токены отмены позволяют вашему коду создавать связанный CancellationTokenSource, который отменяется другим токеном отмены, в дополнение к тому, что сам может запросить отмену:
async Task DoAsync(
CancellationToken ct)
{
using var cts =
CancellationTokenSource
.CreateLinkedTokenSource(ct);
var task = DoOtherAsync(cts.Token);
… // Что-то делаем
… // возможно вызываем cts.Cancel()
await task;
}
Метод DoAsync принимает токен ct — «внешний» токен отмены. Затем он создает CTS, который связан с этим внешним токеном. Когда он вызывает DoOtherAsync, он передает «внутренний» токен из этого связанного CTS.
Если внешний токен когда-либо отменяется, то связанный cts и его внутренний токен (cts.Token) также отменяются. Более того, метод DoAsync имеет возможность явно отменить связанный CTS — в этом случае будет отменён только внутренний токен отмены, оставив внешний отмены неизменным.
То же самое можно сделать с помощью регистраций:
async Task DoAsync(
CancellationToken ct)
{
using var cts = new CancellationTokenSource();
using var reg = ct.Register(cts.Cancel);
var task = DoOtherAsync(cts.Token);
… // Что-то делаем
… // возможно вызываем cts.Cancel()
await task;
}
Действительно, логически это примерно то же самое: вы можете думать о связанном источнике токена отмены как о совершенно обычном источнике токена отмены вместе с регистрацией, которая отменяет его, когда отменяется какой-либо другой токен.
Множественные связи
Внутренний токен отмены выше отменяется, когда отменяется внешний токен или когда его источник явно его отменяет. Аналогично, мы можем передать любое количество токенов отмены в CreateLinkedTokenSource, и токен отмены, который он возвращает, будет отменён, когда отменится любой из внешних токенов.
Продолжение следует…
Источник: https://blog.stephencleary.com/2024/10/cancellation-6-linking.html
👍8
День 2123. #ЗаметкиНаПолях #Cancellation
Отмена. Часть 6: Связывание. Продолжение
Начало
Варианты использования
Внешний токен и внутренний источник отмены могут на самом деле представлять что угодно; связанные токены отмены полезны, когда вам нужно отменить код, если «A или B».
Но наиболее распространённый вариант использования — когда внешний токен представляет запрос отмены конечного пользователя, а внутренний токен - тайм-аут. Например, когда бизнес-логика представляет собой код типа «тайм-аут и повторная попытка», а также позволяет конечному пользователю отменить все повторные попытки одним нажатием кнопки.
Одним из естественных мест, где используется этот тип кода, является Polly. Polly позволит передать внешний токен, который находится под вашим контролем. Затем он передаёт другой токен отмены вашему делегату выполнения; этот внутренний токен контролируется Polly. Конвейеры Polly (например, тайм-аут) могут отменить внутренний токен для отмены вашего делегата. Естественно, если ваш код отменяет внешний токен, переданный Polly, это также перейдет во внутренний токен. То есть, они связаны:
ExecuteRetryTimeoutAsync принимает внешний токен ct и передаёт его Polly. Затем Polly создаёт связанный внутренний токен (который включает поведение конвейера, такое как тайм-аут), и передаёт внутренний токен (token) вашему делегату.
Делегаты, которые вы передаёте Polly, должны следить за токеном, который они получают от Polly, а не за какими-либо другими! Это может оказаться ловушкой, когда вы добавляете конвейеры Polly в существующий код, например, при добавлении тайм-аутов в этот код:
Частая ошибка – забыть обновить использованный токен:
Здесь делегат по-прежнему следит за внешним токеном отмены ct, а должен следить за токеном token:
Окончание следует…
Источник: https://blog.stephencleary.com/2024/10/cancellation-6-linking.html
Отмена. Часть 6: Связывание. Продолжение
Начало
Варианты использования
Внешний токен и внутренний источник отмены могут на самом деле представлять что угодно; связанные токены отмены полезны, когда вам нужно отменить код, если «A или B».
Но наиболее распространённый вариант использования — когда внешний токен представляет запрос отмены конечного пользователя, а внутренний токен - тайм-аут. Например, когда бизнес-логика представляет собой код типа «тайм-аут и повторная попытка», а также позволяет конечному пользователю отменить все повторные попытки одним нажатием кнопки.
Одним из естественных мест, где используется этот тип кода, является Polly. Polly позволит передать внешний токен, который находится под вашим контролем. Затем он передаёт другой токен отмены вашему делегату выполнения; этот внутренний токен контролируется Polly. Конвейеры Polly (например, тайм-аут) могут отменить внутренний токен для отмены вашего делегата. Естественно, если ваш код отменяет внешний токен, переданный Polly, это также перейдет во внутренний токен. То есть, они связаны:
async Task ExecuteRetryTimeoutAsync(
CancellationToken ct)
{
var pipeline = new ResiliencePipelineBuilder()
.AddTimeout(TimeSpan.FromSeconds(10))
.Build();
await pipeline.ExecuteAsync(async token =>
{
/* ваш код тут */
}, ct);
}
ExecuteRetryTimeoutAsync принимает внешний токен ct и передаёт его Polly. Затем Polly создаёт связанный внутренний токен (который включает поведение конвейера, такое как тайм-аут), и передаёт внутренний токен (token) вашему делегату.
Делегаты, которые вы передаёте Polly, должны следить за токеном, который они получают от Polly, а не за какими-либо другими! Это может оказаться ловушкой, когда вы добавляете конвейеры Polly в существующий код, например, при добавлении тайм-аутов в этот код:
async Task ExecuteAsync(CancellationToken ct)
{
for (int i = 0; i != 10; ++i)
await Task.Delay(1000, ct);
}
Частая ошибка – забыть обновить использованный токен:
async Task ExecuteWithTimeoutAsync(
CancellationToken ct)
{
var pipeline = new ResiliencePipelineBuilder()
.AddTimeout(TimeSpan.FromSeconds(10))
.Build();
await pipeline.ExecuteAsync(async token =>
{
// ПЛОХОЙ КОД!!!
for (int i = 0; i != 10; ++i)
await Task.Delay(1000, ct);
}, ct);
}
Здесь делегат по-прежнему следит за внешним токеном отмены ct, а должен следить за токеном token:
async Task ExecuteWithTimeoutAsync(
CancellationToken ct)
{
var pipeline = new ResiliencePipelineBuilder()
.AddTimeout(TimeSpan.FromSeconds(10))
.Build();
await pipeline.ExecuteAsync(async token =>
{
for (int i = 0; i != 10; ++i)
await Task.Delay(1000, token);
}, ct);
}
Окончание следует…
Источник: https://blog.stephencleary.com/2024/10/cancellation-6-linking.html
👍8
День 2124. #ЗаметкиНаПолях #Cancellation
Отмена. Часть 6: Связывание. Окончание
Начало
Продолжение
Не используйте OperationCanceledException.CancellationToken
Рассмотрим снова исходный пример:
Предположим, код вызывает DoAsync и реагирует на отмену:
Замысел кода — сделать что-то особенное, если код отменяется из-за этого конкретного источника отмены. К сожалению, этот код проблематичен в реальном мире; DoAsync может использовать связанный источник токена отмены, в этом случае OperationCanceledException.CancellationToken не будет соответствовать cts.Token, даже если он был источником отмены!
Поэтому не используйте OperationCanceledException.CancellationToken. Правильное решение — проверять, запросил ли этот источник отмену:
То же касается и проверки связанных источников отмены:
С этим кодом та же проблема! Возможно, что DoAsync может сам использовать связанный токен отмены (или будет использовать в будущем). Решение — не использовать OperationCanceledException.CancellationToken:
Итого
В большинстве случаев не приходится использовать связанные токены отмены, но они бывают полезны. Вот, что следует запомнить:
1. Освобождайте (dispose) источники токенов отмены, включая связанные источники токенов отмены.
2. Не используйте OperationCanceledException.CancellationToken; вместо этого используйте IsCancellationRequested.
3. Для любого кода, который имеет несколько токенов в области действия, будьте внимательны к тому, на какой из них вы реагируете.
Источник: https://blog.stephencleary.com/2024/10/cancellation-6-linking.html
Отмена. Часть 6: Связывание. Окончание
Начало
Продолжение
Не используйте OperationCanceledException.CancellationToken
Рассмотрим снова исходный пример:
async Task DoAsync(
CancellationToken ct)
{
using var cts =
CancellationTokenSource
.CreateLinkedTokenSource(ct);
var task = DoOtherAsync(cts.Token);
… // Что-то делаем
… // возможно вызываем cts.Cancel()
await task;
}
Предположим, код вызывает DoAsync и реагирует на отмену:
async Task MainAsync()
{
using var cts =
new CancellationTokenSource();
cts.CancelAfter(2000);
try
{
await DoAsync(cts.Token);
}
catch (OperationCanceledException ex)
// ПЛОХОЙ КОД!!!
when (ex.CancellationToken == cts.Token)
{
Console.WriteLine("Timeout!");
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
}
Замысел кода — сделать что-то особенное, если код отменяется из-за этого конкретного источника отмены. К сожалению, этот код проблематичен в реальном мире; DoAsync может использовать связанный источник токена отмены, в этом случае OperationCanceledException.CancellationToken не будет соответствовать cts.Token, даже если он был источником отмены!
Поэтому не используйте OperationCanceledException.CancellationToken. Правильное решение — проверять, запросил ли этот источник отмену:
async Task MainAsync()
{
…
catch (OperationCanceledException)
when (cts.IsCancellationRequested)
{
Console.WriteLine("Timeout!");
}
…
}
То же касается и проверки связанных источников отмены:
async Task DoAsync(
CancellationToken ct)
{
using var cts =
CancellationTokenSource
.CreateLinkedTokenSource(ct);
cts.CancelAfter(1000);
try
{
await DoOtherAsync(cts.Token);
}
catch (OperationCanceledException ex)
// ПЛОХОЙ КОД!!!
when (ex.CancellationToken == cts.Token)
{
… // делаем что-то при таймауте
throw;
}
}
С этим кодом та же проблема! Возможно, что DoAsync может сам использовать связанный токен отмены (или будет использовать в будущем). Решение — не использовать OperationCanceledException.CancellationToken:
async Task DoOtherAsync(
CancellationToken ct)
{
…
catch (OperationCanceledException ex)
when (cts.IsCancellationRequested)
{
… // делаем что-то при таймауте
throw;
}
}
Итого
В большинстве случаев не приходится использовать связанные токены отмены, но они бывают полезны. Вот, что следует запомнить:
1. Освобождайте (dispose) источники токенов отмены, включая связанные источники токенов отмены.
2. Не используйте OperationCanceledException.CancellationToken; вместо этого используйте IsCancellationRequested.
3. Для любого кода, который имеет несколько токенов в области действия, будьте внимательны к тому, на какой из них вы реагируете.
Источник: https://blog.stephencleary.com/2024/10/cancellation-6-linking.html
👍9