JavaRush /Курси /C# SELF /Скасування операцій: Cance...

Скасування операцій: CancellationToken

C# SELF
Рівень 60 , Лекція 4
Відкрита

1. Знайомство з CancellationToken

Уявіть ситуацію: ви запускаєте тривале завантаження файлу, а потім раптом згадуєте, що інтернет у вас за тарифним планом, і гігабайти на вагу золота. Або користувач запустив обчислення й згодом вирішив, що вони йому не потрібні. Він хоче натиснути «Скасувати» і не чекати завершення. Сучасні застосунки мають бути відгукливими, тож потрібно вміти передавати сигнал про зупинку будь-якої миті й коректно переривати виконання асинхронної операції.

Власне для цього у .NET і існує ціла інфраструктура скасування — CancellationToken.

CancellationToken (буквально «токен скасування») — це спеціальний об’єкт, який ви передаєте усередину своєї тривалої операції. У будь-який момент ви можете надіслати сигнал про скасування через CancellationTokenSource, а операція має періодично перевіряти токен (наприклад, IsCancellationRequested) і коректно завершуватися, за потреби викликаючи ThrowIfCancellationRequested().

Як це влаштовано (коротко)?

  • Є об’єкт CancellationTokenSource, який «генерує» токени й уміє їх скасовувати (метод Cancel()).
  • Власне CancellationToken — це «сигнальний прапорець», який можна передати багатьом операціям (через властивість Token у джерела).
  • Операція періодично перевіряє токен: якщо скасування запитано, вона припиняє роботу (або викидає OperationCanceledException).

Аналогія: уявімо, що ви керівник (ви — це CancellationTokenSource). Ви роздаєте підлеглим «бейджики» (це CancellationToken). Коли вирішуєте все зупинити, піднімаєте червоний прапор — і всі, у кого є бейджик, одразу припиняють роботу.

2. Як використовувати CancellationToken

Створити джерело токена скасування (CancellationTokenSource)

var cts = new CancellationTokenSource();

Отримати сам токен (CancellationToken)

CancellationToken token = cts.Token;

Передати токен в асинхронний метод

Більшість стандартних асинхронних методів .NET приймають параметр типу CancellationToken. Наприклад, HttpClient.GetAsync, Stream.ReadAsync, Task.Delay тощо.

Приклад — затримка зі скасуванням:

await Task.Delay(10000, token); // Чекати 10 секунд — але можна скасувати!

Надіслати запит на скасування (наприклад, за кнопкою або таймером)

cts.Cancel(); // Усі операції, що отримали цей токен, дізнаються про скасування 

Перевіряти токен усередині методу

Усередині власних методів (особливо якщо робота тривала й циклічна) слід регулярно перевіряти прапорець скасування і викидати виняток OperationCanceledException, якщо скасування запитано:

token.ThrowIfCancellationRequested();

Або просто перевіряти властивість:

if (token.IsCancellationRequested)
{
    // Вивільняємо ресурси, виходимо з методу
}

3. Приклад: Додамо скасування в наш навчальний застосунок

Припустімо, у нас є застосунок, який завантажує дані з сайту. Додаймо туди можливість скасовувати завантаження, якщо користувач передумав.

Базовий приклад асинхронного завантаження

using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

class Downloader
{
    public async Task DownloadAsync(string url)
    {
        var client = new HttpClient();
        string content = await client.GetStringAsync(url); // Без скасування
        Console.WriteLine("Завантаження завершено!");
    }
}

Додаємо CancellationToken

public async Task DownloadAsync(string url, CancellationToken token)
{
    var client = new HttpClient();
    string content = await client.GetStringAsync(url, token); // Тепер з підтримкою скасування!
    Console.WriteLine("Завантаження завершено!");
}

Керуємо скасуванням з боку коду, що викликає

static async Task Main(string[] args)
{
    var downloader = new Downloader();
    var cts = new CancellationTokenSource();

    Console.WriteLine("Введіть URL-адресу для завантаження:");
    string url = Console.ReadLine();

    var downloadTask = downloader.DownloadAsync(url, cts.Token);

    Console.WriteLine("Натисніть будь-яку клавішу, щоб скасувати завантаження...");
    Console.ReadKey();

    cts.Cancel(); // Сигнал скасування

    try
    {
        await downloadTask;
    }
    catch (OperationCanceledException)
    {
        Console.WriteLine("Завантаження скасовано користувачем!");
    }
}

Ось так просто! Тепер користувач може перервати операцію будь-якої миті.

4. Взаємодія CancellationTokenSource і методів

flowchart TD
    A["Код користувача (Main)"] -- створює --> B["CancellationTokenSource"]
    B -- видає --> C["CancellationToken"]
    C -- передається --> D["Асинхронна операція"]
    A -- викликає Cancel() --> B
    D -- періодично перевіряє --> C
    C -- повідомляє про скасування --> D
    D -- кидає Exception або завершує роботу --> A

5. Обробка скасування

Коли операція отримує токен скасування, є два варіанти поведінки:

Методи .NET самостійно викинуть виняток.
Якщо ви викликаєте стандартні методи, як-от Stream.ReadAsync, HttpClient.GetAsync або Task.Delay, і передаєте їм токен, — щойно викличете Cancel(), ці методи самостійно викинуть OperationCanceledException. Вам лишається лише перехопити цей виняток.

Власний асинхронний код.
Якщо ви самі реалізуєте «довгу» операцію, наприклад, циклічну обробку чи складні обчислення, ваша відповідальність — регулярно перевіряти token.IsCancellationRequested (або викликати token.ThrowIfCancellationRequested()), щоб коректно реагувати на скасування.

Приклад: «Довга» операція з ручною перевіркою скасування

public async Task CalculatePrimesAsync(int max, CancellationToken token)
{
    for (int i = 2; i < max; i++)
    {
        token.ThrowIfCancellationRequested(); // Перевіряємо скасування

        if (IsPrime(i))
        {
            Console.WriteLine($"Просте число: {i}");
            await Task.Delay(100, token); // Зробимо коротку паузу (можна скасувати)
        }
    }
    Console.WriteLine("Обчислення завершено!");
}

private bool IsPrime(int n)
{
    for (int i = 2; i <= Math.Sqrt(n); i++)
        if (n % i == 0) return false;
    return true;
}

6. Корисні нюанси

Методи й класи, що підтримують CancellationToken

Клас/метод Підтримує CancellationToken? Приклад використання
Task.Delay
await Task.Delay(1000, token);
HttpClient.GetAsync
await client.GetAsync(url, token);
Stream.ReadAsync
await stream.ReadAsync(buffer, 0, len, token);
Task.Run
Task.Run(() => ..., token);
File.ReadAllTextAsync
await File.ReadAllTextAsync(path, token);
Thread.Sleep
Не підтримує; краще використовувати Task.Delay
Ваші методи ✔ (якщо додати підтримку!)
MyLongMethod(token)

Життєвий цикл операції, яку можна скасувати

sequenceDiagram
    participant Користувач
    participant Main
    participant CancellationTokenSource
    participant Асинхронна операція

    Користувач->>Main: Запуск операції
    Main->>CancellationTokenSource: Створення CTS
    Main->>Асинхронна операція: Запуск і передача CancellationToken
    Користувач->>Main: Натискає "Скасувати"
    Main->>CancellationTokenSource: Викликає Cancel()
    Асинхронна операція->>Асинхронна операція: Помічає скасування (\nIsCancellationRequested)
    Асинхронна операція-->>Main: Кидає OperationCanceledException
    Main->>Користувач: Показує повідомлення "Операцію скасовано"

Де це використовується в реальному житті

  • UI-застосунки: Перервати довгі завантаження, обчислення, роботу з файлами, якщо користувач захотів закрити вікно або скасувати дію.
  • Серверні застосунки: Якщо клієнт обірвав з’єднання — краще одразу скасувати обробку запиту, щоб не витрачати ресурси.
  • Обробка великих даних: Задачі можуть бути дуже тривалими — завжди треба давати можливість зупинити обчислення або міграції.
  • Інтеграція з обладнанням: Сканування, друк та інші операції іноді потрібно екстрено перервати — підтримка скасування тут обов’язкова.

7. Типові помилки під час роботи з CancellationToken

Помилка №1: ігнорування перевірки токена.
Якщо операція не перевіряє token.IsCancellationRequested або не викликає ThrowIfCancellationRequested(), вона не зупиниться під час скасування, даремно витрачаючи ресурси.

Помилка №2: неправильна обробка OperationCanceledException.
Якщо не перехопити OperationCanceledException, застосунок може завершитися неочікувано. Завжди використовуйте try-catch для обробки скасування.

Помилка №3: некоректне керування ресурсами під час скасування.
Скасування не відкочує зміни автоматично (наприклад, у файлах чи базах даних). Потрібно вручну очищати ресурси в блоці catch.

Помилка №4: передавання вже скасованого токена.
Якщо токен уже скасовано, метод одразу викине виняток, що може порушити логіку, якщо це не враховано.

1
Опитування
Порівняння Task і Thread, рівень 60, лекція 4
Недоступний
Порівняння Task і Thread
Паралельна обробка даних
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ