Как правильно отменить выполнение Task в C#?

Ответ

Отмена задач в C# реализуется через кооперативную модель с использованием CancellationTokenSource и CancellationToken. Задача не прерывается принудительно, а лишь получает уведомление о запросе отмены и должна корректно завершиться.

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

// Источник токена, который инициирует отмену
var cancellationTokenSource = new CancellationTokenSource();
// Сам токен, который передается в задачу
CancellationToken cancellationToken = cancellationTokenSource.Token;

// Задача, которая отслеживает запрос на отмену
var longRunningTask = Task.Run(() =>
{
    for (int i = 0; i < 1000; i++)
    {
        // Способ 1: Выбросить OperationCanceledException
        cancellationToken.ThrowIfCancellationRequested();

        // Способ 2: Проверить флаг и выйти корректно
        if (cancellationToken.IsCancellationRequested)
        {
            // Выполнить cleanup (закрыть файлы, соединения)
            Console.WriteLine("Задача завершается по отмене.");
            return; // Просто выходим из метода, задача перейдет в состояние RanToCompletion
        }

        // Полезная работа
        Thread.Sleep(100); // Имитация работы
        Console.WriteLine($"Итерация {i}");
    }
}, cancellationToken); // Важно передать токен в Task.Run для привязки

// ... где-то в другом месте (по кнопке пользователя, таймауту)
cancellationTokenSource.CancelAfter(TimeSpan.FromSeconds(2)); // Автоотмена через 2 сек
// или
// cancellationTokenSource.Cancel(); // Немедленная отмена

try
{
    await longRunningTask; // Если задача отменена через ThrowIfCancellationRequested,
                           // здесь будет выброшено AggregateException -> OperationCanceledException
}
catch (OperationCanceledException)
{
    Console.WriteLine("Задача была отменена.");
}

2. Отмена с таймаутом

// Создать источник с таймаутом при создании
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
await SomeAsyncOperation(cts.Token);

// Или установить таймаут позже
cts.CancelAfter(5000);

3. Объединение нескольких токенов Полезно, когда задача должна отреагировать на отмену из нескольких источников (например, пользовательский запрос + общий таймаут).

var userCancellationSource = new CancellationTokenSource();
var timeoutSource = new CancellationTokenSource(TimeSpan.FromSeconds(30));

// Создаем linked token source, который сработает при отмене любого из токенов
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
    userCancellationSource.Token,
    timeoutSource.Token
);

await ProcessDataAsync(linkedCts.Token);

4. Отмена асинхронных операций (HttpClient, EF Core) Большинство современных асинхронных API в .NET принимают CancellationToken.

public async Task<string> DownloadWithTimeoutAsync(string url, CancellationToken cancellationToken)
{
    using var httpClient = new HttpClient();
    using var response = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
    response.EnsureSuccessStatusCode();
    return await response.Content.ReadAsStringAsync(cancellationToken);
}

// Использование
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
try
{
    string data = await DownloadWithTimeoutAsync("https://api.example.com/data", cts.Token);
}
catch (TaskCanceledException) // HttpClient выбрасывает TaskCanceledException при отмене
{
    Console.WriteLine("Запрос отменен по таймауту.");
}

5. Состояние задачи после отмены

  • Если задача завершилась вызовом ThrowIfCancellationRequested(), ее свойство Status будет равно TaskStatus.Canceled.
  • Если задача проверила IsCancellationRequested и вышла через return, ее статус будет TaskStatus.RanToCompletion.
  • Задачу в состоянии Canceled или Faulted нельзя awaitить повторно — будет исключение.

Главное правило: Всегда проектируйте длительные операции с возможностью кооперативной отмены и передавайте CancellationToken в глубину вызовов.

Ответ 18+ 🔞

Ну что за дичь, опять эти токены отмены! Слушай, я тебе сейчас на пальцах объясню, как эту хрень использовать, чтобы не выстрелить себе в ногу.

Вот смотри, в C# тебе не дадут просто так взять и убить задачу — это не какой-то там варварский Thread.Abort(), который всё ломает к чертям. Тут всё по-взрослому, через вежливые уведомления. Есть два главных героя: CancellationTokenSource (тот, кто кричит "отмена!") и сам CancellationToken (флажок, который ты передаёшь в задачу).

1. Базовый сценарий, чтоб ты понял суть

// Это наш пульт управления отменой, с красной кнопкой
var cts = new CancellationTokenSource();
// А это сам флажок-уведомление, который мы будем тыкать в задачу
var token = cts.Token;

var task = Task.Run(() =>
{
    for (int i = 0; i < 1000; i++)
    {
        // Способ первый, радикальный: проверяем и сразу в исключение
        token.ThrowIfCancellationRequested(); // Если отмена запрошена — летит OperationCanceledException

        // Способ второй, аккуратный: проверили и вышли по-тихому
        if (token.IsCancellationRequested)
        {
            Console.WriteLine("Всё, приехали, завершаемся.");
            // Тут можно прибраться за собой — закрыть файлы, отключиться от БД
            return; // Просто выходим, задача будет считаться успешно завершённой
        }

        // Делаем вид, что работаем
        Thread.Sleep(100);
        Console.WriteLine($"Кручу-верчу, итерация {i}");
    }
}, token); // ОБЯЗАТЕЛЬНО суём токен сюда, это важно для правильного статуса задачи

// Допустим, прошло 2 секунды и нам надоело ждать
cts.CancelAfter(2000);
// Или можем отменить сразу: cts.Cancel();

try
{
    await task; // Если задача отменена через ThrowIfCancellationRequested, тут будет исключение
}
catch (OperationCanceledException)
{
    Console.WriteLine("Задачу отменили, как и просили.");
}

2. Таймаут — чтоб не ждать до второго пришествия

// Можно сразу создать источник с таймаутом
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
await SomeLongOperation(cts.Token);

// Или назначить таймаут потом
cts.CancelAfter(5000); // Через 5 секунд само отменится

3. Объединение токенов — когда на тебя орут сразу несколько

Бывает, что задача должна слушаться и пользователя (нажал "отмена"), и общего таймаута системы.

var userCts = new CancellationTokenSource(); // Пользовательская отмена
var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(30)); // Системный таймаут

// Связываем их вместе — сработает любой
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
    userCts.Token,
    timeoutCts.Token
);

await SuperImportantProcess(linkedCts.Token); // Передаём объединённый токен

4. Асинхронные операции — там всё уже готово

Все нормальные асинхронные методы в .NET (HttpClient, EF Core, файловые операции) принимают CancellationToken. Пользуйся!

public async Task<string> DownloadWithTimeout(string url, CancellationToken ct)
{
    using var httpClient = new HttpClient();
    // Передаём токен прямо в GetAsync — если отменят, запрос прервётся
    using var response = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead, ct);
    response.EnsureSuccessStatusCode();
    return await response.Content.ReadAsStringAsync(ct); // И здесь тоже!
}

// Используем
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
try
{
    var data = await DownloadWithTimeout("https://example.com", cts.Token);
}
catch (TaskCanceledException) // HttpClient кидает именно TaskCanceledException при отмене
{
    Console.WriteLine("Не успел, блядь, отменили по таймауту!");
}

5. Важный нюанс про состояние задачи

Запомни раз и навсегда:

  • Если задача упала через ThrowIfCancellationRequested() — её статус будет TaskStatus.Canceled.
  • Если она сама проверила IsCancellationRequested и вышла через return — статус TaskStatus.RanToCompletion (успех!).
  • Задачу в состоянии Canceled или Faulted нельзя повторно awaitить — получишь исключение, как от скучной лекции.

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