CodeGym /Kursy /C# SELF /Klasy Task i

Klasy Task i Task<TResult>

C# SELF
Poziom 60 , Lekcja 0
Dostępny

1. Wprowadzenie

W świecie programowania asynchronicznego żyjemy zgodnie z zasadą "delegowania z informacją zwrotną". Wyobraź sobie: musisz pobrać ogromny plik, przeanalizować gigabajty logów lub wysłać żądanie do odległego serwera. Zamiast stać w oczekiwaniu jak posąg, mówimy systemowi: "Zajmij się tym, a ja tymczasem popracuję nad czymś innym. Jak skończysz — koniecznie daj znać!"

I właśnie tutaj na scenę wchodzi Task — eleganckie ucieleśnienie takiego podejścia. To nie tylko techniczna abstrakcja, to coś w rodzaju "sprytnego pośrednika", który bierze na siebie odpowiedzialność za wykonanie pracy i gwarantuje, że wynik nie zaginie w cyfrowej próżni.

Task działa jak osobisty asystent, któremu zlecasz ważne zadanie. On kiwa, zapisuje zadanie w swoim notesie i mówi: "Idź spokojnie robić swoje, ja dam znać, kiedy wszystko będzie gotowe". I faktycznie daje znać — z wynikiem w ręku albo z uczciwym wyjaśnieniem, dlaczego coś poszło nie tak.

Jeśli szukać bardziej codziennej analogii, to Task przypomina nowoczesny system rejestracji: rezerwujesz wizytę online, dostajesz potwierdzenie i nie musisz przesiedzieć godzin w kolejce. System sam przypomni o zbliżającej się wizycie, a ty w międzyczasie możesz normalnie żyć.

Klasa Task

Task — to podstawowy budulec programowania asynchronicznego w .NET. Reprezentuje uruchomioną lub przyszłą operację, której wynik będzie dostępny w przyszłości. Jeśli metoda nic nie ma zwracać, używamy po prostu Task.

public async Task BackupToCloudAsync()
{
    // Robi magię backupu, nic nie zwraca
}

Klasa Task<TResult>

Jeśli trzeba zwrócić wynik (np. string, liczbę, obiekt…), używamy Task<TResult>:

public async Task<string> DownloadHtmlAsync(string url)
{
    // Pobiera stronę i zwraca HTML
    return "<html>...</html>";
}

Dlaczego Task, a nie Thread?

Thread zarządza samym wątkiem (to ciężkie i niebezpieczne), a Task — to wyższy poziom abstrakcji: może wykonywać się w puli wątków, może działać asynchronicznie bez tworzenia nowego wątku (np. przy operacjach I/O) i nie wymaga od ciebie martwienia się o niskopoziomowe szczegóły.

Klasa Task pozwala opisać: "Chcę uruchomić to działanie", a jak dokładnie zostanie wykonane — niech zdecyduje .NET!

2. Budowa obiektu Task

Właściwości i metody Task, które warto znać

Właściwość / Metoda Opis
Status
Bieżący stan zadania
Result
Wynik dla Task<TResult> (blokuje wątek)
IsCompleted
Czy zadanie zostało zakończone
IsFaulted
Czy w zadaniu wystąpił wyjątek
IsCanceled
Czy zadanie zostało anulowane
Wait()
Blokuje bieżący wątek do zakończenia (niebezpieczne)
ContinueWith()
Uruchamia kolejne zadanie po zakończeniu
Exception
Dostęp do wyjątku, jeśli Task zakończył się błędem
Id
Unikalny identyfikator zadania

Jak działa metoda asynchroniczna zwracająca Task

sequenceDiagram
    participant Main as Main Wątek
    participant Task as Task (Zadanie w tle)
    Main->>Task: Uruchomienie Task.Run(() => ...)
    Note right of Task: Wykonywanie w tle
(CPU lub I/O) alt Zadanie zakończone Task->>Main: await zakończył się, kontynuujemy dalej else Błąd Task->>Main: await rzuca wyjątek end

3. Tworzenie i uruchamianie zadań: jak działa Task

Metody asynchroniczne z async

Najczęstszy przypadek — po prostu deklarujesz metodę jako async i zwracasz albo Task, albo Task<TResult> (jak pokazaliśmy wcześniej).

Task.Run: wykonanie w puli wątków

Jeśli potrzebujesz wykonać jakąś ciężką pracę w tle (np. skomplikowane obliczenia albo kodowanie wideo), możesz użyć Task.Run:

Task work = Task.Run(() =>
{
    // Ciężkie obliczenia — nie blokujemy głównego wątku!
    Console.WriteLine("Obliczenia w tle rozpoczęte...");
    Thread.Sleep(2000); // Emulacja długiej pracy
    Console.WriteLine("Obliczenia w tle zakończone!");
});

Jeśli przydatne jest otrzymanie wyniku:

Task<int> calculateTask = Task.Run(() =>
{
    // Na przykład sumowanie pierwszych 100 liczb
    int sum = 0;
    for (int i = 1; i <= 100; i++) sum += i;
    return sum;
});

Task.Factory.StartNew

To bardziej niskopoziomowy i elastyczny sposób, pozwala precyzyjnie konfigurować uruchomienie zadania (np. wskazać scheduler, przekazać parametry itp.). W nowoczesnym kodzie prawie zawsze zaleca się używać Task.Run, bo jest prostszy i chroni przed błędami.

4. Aplikacja dnia: nasz katalog książek

Załóżmy, że mamy aplikację katalogu książek i musimy dodać funkcję ładowania książek ze "źródła w chmurze" — to będzie operacja I/O-bound (wolne żądanie HTTP lub czytanie z pliku).

Dodajmy metodę, która asynchronicznie "ładuje" książki (emulujemy opóźnienie):

public class Book
{
    public string Title { get; set; }
    public string Author { get; set; }
}

public class BookCatalog
{
    public List<Book> Books { get; set; } = new();

    public async Task LoadBooksAsync()
    {
        Console.WriteLine("Ładowanie książek...");
        await Task.Delay(2000); // Emulacja długiego ładowania (np. HTTP lub plik)
        Books = new List<Book>
        {
            new Book { Title = "CLR via C#", Author = "Jeffrey Richter" },
            new Book { Title = "C# in Depth", Author = "Jon Skeet" }
        };
        Console.WriteLine("Książki pomyślnie załadowane.");
    }
}

W Main wywołamy asynchroniczne ładowanie (używając await):

var catalog = new BookCatalog();
await catalog.LoadBooksAsync();
Console.WriteLine($"W katalogu {catalog.Books.Count} książek.");

Tabela: Główne sposoby tworzenia i uruchamiania Task

Metoda tworzenia Jak stosować Wynik Zastosowanie
async-metoda
async Task / async Task<T>
Operacja asynchroniczna Zazwyczaj I/O, wygoda
Task.Run
Task.Run(() => { ... })
Zadanie w tle CPU-bound (obliczenia)
TaskCompletionSource<T>
Ręcznie tworzymy i kończymy Task Pełna kontrola programisty Rzadko, do niskopoziomowych rzeczy

5. Cykl życia Task

Task może być w różnych stanach:

  • Created — zadanie utworzone, ale nie uruchomione (dla Task z wyraźnym uruchomieniem).
  • WaitingToRun — czeka w kolejce w puli.
  • Running — wykonywane.
  • WaitingForActivation — czeka na uruchomienie lub zewnętrzną aktywację.
  • RanToCompletion — pomyślnie zakończone.
  • Faulted — zakończone z błędem (wyjątkiem).
  • Canceled — anulowane (jeśli obsługiwana jest anulacja).

Diagram

flowchart LR
    Start -->|Uruchomienie zadania| Running
    Running -->|Powodzenie| Completed
    Running -->|Błąd| Faulted
    Running -->|Anulowanie| Canceled

Sprawdźmy to w praktyce

Task task = Task.Run(() =>
{
    Thread.Sleep(1000);
});
Console.WriteLine(task.Status); // Zwykle: Running albo WaitingToRun
await task;
Console.WriteLine(task.Status); // RanToCompletion po zakończeniu

6. Jak uzyskać wynik z Task<TResult>?

Task<TResult> — to opakowanie nad wynikiem, który pojawi się w przyszłości. Gdy trzeba poczekać na wynik, używamy await:

Task<int> sumTask = Task.Run(() =>
{
    int sum = 0;
    for (int i = 1; i <= 5; i++) sum += i;
    return sum;
});

int result = await sumTask;
Console.WriteLine(result); // 15

Jeśli zapomnisz napisać await, otrzymasz Task (obietnicę), a nie wynik. To typowa "asynchroniczna pułapka".

Alternatywa: Synchroniczne pobranie wyniku (NIE RÓB TEGO W UI!)

Czasami (np. w testach) trzeba dostać wynik bez await. Można użyć właściwości .Result:

int result = sumTask.Result;

Ale jeśli Task jeszcze się nie zakończył, ten kod blokuje wątek, i jeśli to wątek UI, aplikacja zamarznie! Dlatego: lepiej zawsze preferować await.

Typowe błędy z Task i Task<TResult>

Zapomniano zwrócić Task, metoda stała się void. Jeżeli metoda nie ma zwracanego wyniku — zwracaj Task, nie void, bo inaczej nie będzie można obsłużyć błędu.

Ignorowanie await. Po prostu wywołano metodę, nie czekając na wynik, i zadanie zaczyna żyć własnym życiem ("fire and forget"). Nie dowiesz się, kiedy się skończy ani czy upadło z błędem.

Blokujące oczekiwanie przez .Result lub .Wait(). Łatwo otrzymać deadlock, szczególnie w UI i ASP.NET. Używaj tylko await.

7. Zaawansowane możliwości Task

Łańcuch zadań: ContinueWith

Można "podczepiać" akcje, które trzeba wykonać po zakończeniu zadania, przez ContinueWith:

Task.Run(() => 10)
    .ContinueWith(t =>
    {
        Console.WriteLine($"Wykonano! Wynik: {t.Result}");
    });

Ale w nowoczesnym C# zwykle robi się to za pomocą async/await — tak łatwiej czytać.

Przykład: Równoległe i sekwencyjne ładowanie danych

Załóżmy, że musisz pobrać dwie książki z różnych źródeł. Możesz uruchomić oba Task równolegle i poczekać na oba:

public async Task LoadBooksFromMultipleSourcesAsync()
{
    Task<List<Book>> t1 = LoadFromCloudAsync();
    Task<List<Book>> t2 = LoadFromLocalAsync();

    // Czekamy na oba zadania równolegle
    await Task.WhenAll(t1, t2);

    // Łączymy wyniki
    Books = t1.Result.Concat(t2.Result).ToList();
}

private async Task<List<Book>> LoadFromCloudAsync()
{
    await Task.Delay(2000); // "Chmura"
    return new List<Book> { new Book { Title = "Cloud Book", Author = "Cloud Author" } };
}

private async Task<List<Book>> LoadFromLocalAsync()
{
    await Task.Delay(1000); // "Dysk lokalny"
    return new List<Book> { new Book { Title = "Local Book", Author = "Local Author" } };
}

Zwróć uwagę: dzięki await Task.WhenAll(...) oba zapytania startują jednocześnie i wykonują się równolegle (jeśli to możliwe), a aplikacja czeka, aż oba się zakończą.

8. Przydatne niuanse

Task i Fire-and-forget

Czasem chcesz uruchomić zadanie i nie czekać, aż się skończy (np. wysłać logi do chmury albo "podpiec tosta", podczas gdy użytkownik pracuje):

async void LogToCloudAsync(string message)
{
    await Task.Run(() =>
    {
        // Długie wysyłanie logu
        Thread.Sleep(1000);
        Console.WriteLine($"Log wysłany: {message}");
    });
}

Ale pamiętaj: jeśli w takim zadaniu wystąpi błąd — trudno będzie się o tym dowiedzieć. Dlatego jeśli to możliwe, zwracaj Task i przynajmniej loguj wyjątki wewnątrz!

Task i Task<TResult> w realnym świecie

  • W aplikacjach klienckich UWP/WPF/WinForms nie blokuj UI — używaj Task do długich operacji (pliki, sieć).
  • W WebAPI/ASP.NET Task pomaga nie marnować wątków na czekanie na sieć/DB, poprawiając wydajność.
  • Organizuj wykonanie "równoległe": jednocześnie pobierz, przetwórz i zapisz.
  • Prawie wszystkie długie metody mają wariant Async: File.ReadAllTextAsync, HttpClient.GetStringAsync i inne.

FAQ i niespodzianki

Pytanie: Dlaczego Task czasem wykonuje się synchronicznie?
Odpowiedź: Jeśli operacja jest już zakończona (np. wynik jest zcache'owany), kompilator i/lub scheduler może zakończyć metodę na tym samym wątku synchronicznie. To normalne i przyspiesza powtórne wywołania.

Pytanie: Dlaczego nie używać async void?
Odpowiedź: Taka metoda nie może być "czekana", nie da się złapać jej błędów ani śledzić zakończenia. Używaj Task, a async void — tylko dla EventHandler (np. Button_Click).

Pytanie: Czy można uruchomić kilka zadań i czekać tylko na jedno?
Odpowiedź: Tak — użyj Task.WhenAny.

Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION