Как корректно отменить выполнение асинхронной задачи (Task) в 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.

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

Ответ 18+ 🔞

Слушай, про отмену асинхронных операций в C# — это ж классика, как водка под селёдку. Весь этот цирк с CancellationTokenSource и CancellationToken называется Cooperative Cancellation, что в переводе на русский значит «отмена по-хорошему, без мордобоя». То есть мы не прибиваем поток дубиной, а вежливо просим: «Слышь, братан, может, уже хватит?»

Суть, если по-простому

  1. Заводишь источник отмены: CancellationTokenSource cts = new(); — это как дистанционка от телевизора, только для операции.
  2. Выдёргиваешь из него токен: CancellationToken token = cts.Token; — это уже сама кнопка, которую ты суёшь в метод.
  3. Метод работает и периодически смотрит на эту кнопку: «Меня уже не отменили?»
  4. Когда надо остановиться — жмёшь cts.Cancel(): Или ставишь таймер через CancelAfter, чтобы само отрубилось, как духовка.
  5. Метод видит, что кнопку нажали, и начинает аккуратно сворачиваться, а не бросает всё на полпути.

Ну, пример, чтобы совсем понятно было

Вот смотри, есть у нас метод, который делает вид, что работает:

public async Task ProcessDataAsync(CancellationToken cancellationToken)
{
    for (int i = 0; i < 100; i++)
    {
        // Способ 1: Жёсткий. Если отмена запрошена — сразу вылетает исключение.
        cancellationToken.ThrowIfCancellationRequested();

        // Способ 2: Мягкий. Можно проверить флаг и сделать что-то умное перед выходом.
        if (cancellationToken.IsCancellationRequested)
        {
            Console.WriteLine("Ладно, ладно, уже выхожу...");
            // Тут по-хорошему закрываешь файлы, откатываешь транзакции, ну ты понял.
            break;
        }

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

// А вот как этим пользоваться
async Task MainMethod()
{
    using var cts = new CancellationTokenSource(); // using — чтобы не было утечек, это важно!
    cts.CancelAfter(TimeSpan.FromSeconds(5)); // Автоматически отменится через 5 секунд, даже если ты уснёшь.

    try
    {
        await ProcessDataAsync(cts.Token);
        Console.WriteLine("Всё сделал, начальник!");
    }
    catch (OperationCanceledException) // Ловишь именно это исключение, оно тут главное.
    {
        Console.WriteLine("Операцию отменили, да похуй.");
    }
}

Главные фишки, которые надо запомнить

  • Токен — как эстафетная палочка. Ты получил его сверху — передавай дальше, во все методы, которые его поддерживают. Не жадничай.
  • Фреймворк — не дурак. Куча стандартных методов (HttpClient.SendAsync, чтение файлов, работа с БД) уже имеют перегрузки с CancellationToken. Всегда, блядь, используй их! Это чтобы если пользователь закрыл окно, твой запрос не висел до скончания времён.
  • CancellationTokenSource — одноразовый шприц. После Cancel() его использовать нельзя. Создавай новый. И оборачивай в using, он таймеры внутри держит, чтобы ресурсы не текли.
  • Исключения. ThrowIfCancellationRequested() кидает OperationCanceledException. Некоторые либы (тот же HttpClient) могут кидать TaskCanceledException (он от него наследуется). Так что лови обычно OperationCanceledException, и всё будет ок.

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