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

«Как правильно отменить выполнение Task в C#?» — вопрос из категории Многопоточность, который задают на 25% собеседований 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 в глубину вызовов.