Как корректно отменить выполнение асинхронной задачи (Task) в C#?

«Как корректно отменить выполнение асинхронной задачи (Task) в C#?» — вопрос из категории Многопоточность, который задают на 25% собеседований C# Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Для отмены асинхронных операций в C# используется паттерн Cooperative Cancellation на основе CancellationTokenSource и CancellationToken. Отмена является запросом, а не принудительной остановкой потока.

Базовый паттерн

  1. Создание источника отмены: CancellationTokenSource cts = new();
  2. Получение токена: CancellationToken token = cts.Token;
  3. Передача токена в асинхронный метод.
  4. Запрос отмены: Вызов cts.Cancel() или cts.CancelAfter(timeout).
  5. Реакция на отмену внутри метода: периодическая проверка token.ThrowIfCancellationRequested() или token.IsCancellationRequested.

Пример: Отмена длительной операции

public async Task ProcessDataAsync(CancellationToken cancellationToken)
{
    for (int i = 0; i < 100; i++)
    {
        // 1. Проверяем запрос на отмену. Если он был, выбрасывается OperationCanceledException.
        cancellationToken.ThrowIfCancellationRequested();

        // 2. Или проверяем флаг для более гибкой логики (например, graceful shutdown).
        if (cancellationToken.IsCancellationRequested)
        {
            // Выполняем очистку ресурсов...
            break;
        }

        // Полезная работа...
        await Task.Delay(100, cancellationToken); // Task.Delay также поддерживает отмену.
    }
}

// Использование
async Task MainMethod()
{
    using var cts = new CancellationTokenSource();
    cts.CancelAfter(TimeSpan.FromSeconds(5)); // Автоотмена через 5 секунд.

    try
    {
        await ProcessDataAsync(cts.Token);
        Console.WriteLine("Операция завершена успешно.");
    }
    catch (OperationCanceledException) // Ловим именно этот тип исключения.
    {
        Console.WriteLine("Операция была отменена.");
    }
}

Ключевые моменты

  • Распространение токена: Все асинхронные методы в цепочке вызовов должны принимать и передавать CancellationToken дальше, если это возможно.
  • Поддержка в библиотеках: Многие методы в .NET (например, HttpClient.SendAsync, Stream.ReadAsync, DbContext.SaveChangesAsync) имеют перегрузки с параметром CancellationToken. Всегда используйте их.
  • Использование using: CancellationTokenSource реализует IDisposable. Всегда оборачивайте его в using или вызывайте Dispose() для своевременного освобождения ресурсов таймера.
  • Исключение TaskCanceledException: Является производным от OperationCanceledException. Некоторые API (например, HttpClient) бросают именно его. Ловите базовый OperationCanceledException.

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