Отмена. Часть 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