1. Wprowadzenie
Kiedy uruchamiasz operację asynchroniczną (lub długotrwałą), użytkownik (albo inny kod) może nagle zdecydować: "Stop! Już nie trzeba! Zatrzymaj się!". Na przykład użytkownik przerwał pobieranie ogromnego pliku, zamknął okno programu lub zmienił zdanie o wyszukiwaniu informacji w dużej bazie danych. Bez obsługi anulowania Twoja aplikacja może dalej działać i pochłaniać zasoby na darmo — to nie najlepszy sposób dbania o użytkownika i komputer.
Typowe scenariusze anulowania:
- Anulowanie pobierania pliku lub anulowanie wysyłania danych.
- Szybkie wyjście z ciężkiego przetwarzania danych na żądanie użytkownika.
- W razie nagłego wstrzymania/zatrzymania długiej pracy, która już nie jest aktualna.
Anulowanie — Twój sekretny składnik do tworzenia przyjaznych, responsywnych i oszczędnych aplikacji.
Jak anulować operacje asynchroniczne w .NET?
W .NET do anulowania długotrwałych zadań używa się koncepcji "tokenu anulowania" (CancellationToken). To specjalny obiekt, który przekazywany jest do wszystkich komponentów zadania. Jeśli ktoś poprosi o anulowanie operacji — token od razu to przekazuje wszystkim zainteresowanym częściom programu. W praktyce to działa jak czerwony sztandar: kto pierwszy zobaczy — ten się zatrzyma.
W .NET mechanizm ten jest zrealizowany przy pomocy dwóch kluczowych klas:
- CancellationTokenSource — tworzy token anulowania i nim zarządza.
- CancellationToken — przekazywany do metod asynchronicznych, żeby można je było anulować.
Ważne: sam token anulowania nie przerywa wykonywania kodu, tylko "sygnałuje" — a Twoja aplikacja decyduje, kiedy i jak zareagować na ten sygnał.
2. Tworzymy token anulowania i anulujemy zadanie
Pokażemy, jak to działa, na prostym przykładzie (rozwiniemy nasze edukacyjne konsolowe demo).
Przykład: prosta operacja asynchroniczna z anulowaniem
using System;
using System.Threading;
using System.Threading.Tasks;
namespace DemoApp
{
class Program
{
static async Task Main()
{
// Tworzymy źródło tokenu anulowania
CancellationTokenSource cts = new CancellationTokenSource();
// Uruchamiamy zadanie asynchroniczne
Task longRunningTask = DoWorkAsync(cts.Token);
Console.WriteLine("Naciśnij dowolny klawisz, żeby anulować operację...");
Console.ReadKey();
// Żądamy anulowania
cts.Cancel();
try
{
await longRunningTask;
}
catch (OperationCanceledException)
{
Console.WriteLine("Operacja została anulowana!");
}
}
// Metoda asynchroniczna wspierająca anulowanie
static async Task DoWorkAsync(CancellationToken cancellationToken)
{
for (int i = 0; i < 10; i++)
{
// Sprawdzamy sygnał anulowania
cancellationToken.ThrowIfCancellationRequested();
Console.WriteLine($"Wykonywanie kroku {i + 1}/10...");
await Task.Delay(1000); // Opóźnienie 1 sekunda
}
Console.WriteLine("Operacja zakończona pomyślnie!");
}
}
}
Jak to działa?
- Tworzymy CancellationTokenSource (cts), skąd bierzemy token (cts.Token).
- Przekazujemy ten token do naszej asynchronicznej operacji.
- Wewnątrz DoWorkAsync() regularnie sprawdzamy token na anulowanie metodą ThrowIfCancellationRequested(). Jeśli użytkownik poprosi o anulowanie zadania — metoda rzuci wyjątek OperationCanceledException, i zadanie zostanie przerwane.
- W Main() czekamy na naciśnięcie klawisza i wywołujemy cts.Cancel(), żeby "zasygnalizować" potrzebę zatrzymania operacji.
Jeśli nie będziesz sprawdzać cancellationToken.IsCancellationRequested lub nie wywołasz ThrowIfCancellationRequested(), Twoje zadanie będzie dalej działać, jak gdyby nic się nie stało — token to tylko informacyjny sztandar.
3. CancellationToken: jak to działa? I trochę magii
Token anulowania to obiekt, który łatwo przekazać między metodami i zadaniami. To daje dużą elastyczność:
- Ten sam token można użyć w kilku operacjach asynchronicznych i synchronicznych.
- Można zorganizować "grupowe" anulowanie dla wszystkich zadań, jeśli kilka operacji korzysta z tokenu z jednego CancellationTokenSource.
- Token anulowania jest dyskretny: nawet jeśli go zignorujesz, kod będzie działać dalej jak wcześniej.
Zarządzanie anulowaniem: gdzie i jak sprawdzać token?
Sprawdzać, czy został podniesiony "sztandar anulowania", można i trzeba tam, gdzie to logicznie uzasadnione: w pętli, na każdym kroku długiego przetwarzania, przy przejściu między etapami itp.
// W dowolnym miejscu sprawdzenia
if (cancellationToken.IsCancellationRequested)
{
Console.WriteLine("Operacja anulowana! Wychodzimy...");
return;
}
// Albo tak (krótko i z rzuconym wyjątkiem)
cancellationToken.ThrowIfCancellationRequested();
Zwykle używa się ThrowIfCancellationRequested() — on rzuca specjalny wyjątek, który można złapać w kodzie wywołującym.
4. Metody asynchroniczne biblioteki standardowej
Wiele klas i metod .NET (szczególnie asynchronicznych) obsługuje CancellationToken od razu "po wyjęciu z pudełka". Warto ich używać, żeby poprawnie zatrzymywać operacje.
Oto przykład z asynchronicznym czytaniem pliku:
using System.IO;
using System.Threading;
using System.Threading.Tasks;
class FileDemo
{
public static async Task ReadFileWithCancelAsync(string filePath, CancellationToken cancellationToken)
{
using FileStream stream = File.OpenRead(filePath);
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) > 0)
{
// Przetwarzamy dane...
// Jeśli token anulowania został zażądany, ReadAsync sam rzuci OperationCanceledException
}
}
}
Więcej o FileStream.ReadAsync i CancellationToken przeczytasz w oficjalnej dokumentacji.
5. Przydatne niuanse
Timeout — to też anulowanie!
Możesz automatycznie anulować operacje po upływie określonego czasu. Do tego źródło tokenu można "zaprogramować":
// Utworzyć CancellationTokenSource z timeoutem (np. 5 sekund)
CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
Po 5 sekundach token zostanie automatycznie "podniesiony", i wszystkie operacje z nim powiązane zatrzymają się przy następnym sprawdzeniu. To wygodne, gdy nie chcesz czekać w nieskończoność.
Co się dzieje przy anulowaniu?
Kiedy wywołujesz Cancel() na źródle tokenu, wszystkie metody które używają tego tokenu i sprawdzają jego stan dowiedzą się o tej zmianie. Ale jeśli Twój kod nie sprawdza tokenu — anulowanie nie nastąpi.
Typowy błąd: zapomnieć przekazać token do wszystkich asynchronicznych i długich operacji. Wtedy część operacji zostanie anulowana, a część będzie dalej działać, jakby nic się nie stało.
Wizualizacja: jak przebiega anulowanie
sequenceDiagram
participant Main as Główny wątek
participant CTS as CancellationTokenSource
participant Task as Zadanie asynchroniczne
Main->>CTS: tworzy CTS, dostaje token
Main->>Task: przekazuje token do zadania asynchronicznego
Note over Task: Zadanie okresowo sprawdza token
Main->>CTS: wywołuje Cancel()
CTS-->>Task: token dostaje status "anulowano"
Task-->>Main: rzuca OperationCanceledException
Main->>Main: łapie wyjątek i kończy działanie
Gdzie stosuje się anulowanie operacji asynchronicznych?
- Asynchroniczne pobierania i zapytania do serwera: można anulować przy słabym internecie lub zniecierpliwieniu użytkownika.
- Duże obliczenia: można zatrzymać po timeoutcie lub na życzenie użytkownika.
- Operacje sieciowe, praca z plikami, przetwarzanie dużych kolekcji w tle.
Na tym kończymy wprowadzenie do anulowania operacji asynchronicznych — teraz Twoja aplikacja będzie nie tylko szybka, ale i troskliwa!
6. Wskazówki i typowe błędy
Nie zapominaj przekazywać tokenu anulowania do wszystkich metod i wywołań, które obsługują cancellation. Jeśli gdzieś nie przekażesz tokenu — operacja może "zawiesić się" i nie zatrzymać.
Sprawdzaj token regularnie — szczególnie w długich pętlach, przy przetwarzaniu plików lub przy pobieraniu dużych ilości danych. Używaj IsCancellationRequested lub ThrowIfCancellationRequested().
Nie próbuj "na siłę" kończyć wątku lub zadania z zewnątrz: token anulowania to prośba o zatrzymanie, a nie młotek na wątek.
Funkcje biblioteki standardowej, takie jak ReadAsync, Delay, HttpClient.SendAsync i wiele innych, już wspierają anulowanie przez token. Korzystaj z tego!
Przy obsłudze anulowania łap konkretnie OperationCanceledException — to specjalny wyjątek mówiący o poprawnym anulowaniu na żądanie.
GO TO FULL VERSION