CodeGym /Kursy /C# SELF /Priorytety wątków i typy wątków

Priorytety wątków i typy wątków

C# SELF
Poziom 55 , Lekcja 3
Dostępny

1. Wprowadzenie

Wyobraźmy sobie, że wątki to ludzie stojący w kolejce po lody. Ktoś stoi spokojnie, a ktoś robi zamieszanie ("Muszę zdążyć na pociąg, przepuśćcie mnie!"), i sprzedawca niby słyszy wszystkich, ale czasem decyduje się obsłużyć kogoś poza kolejnością — np. mamę z płaczącym dzieckiem. Priorytety wątków to mniej więcej ta sama historia.

W C# (dokładniej w .NET) każdy wątek ma swój priorytet — to wskazówka dla systemu operacyjnego, jak bardzo dany wątek jest "ważny" w porównaniu z innymi. To nie jest żelazne prawo dla OS (nikt nie gwarantuje, że najbardziej uprzywilejowany wątek zawsze dostanie całą uwagę), ale zwykle wątki o wyższym priorytecie dostają więcej czasu procesora.

Gdzie to naprawdę się przydaje?

  • W interfejsach użytkownika: np. aktualizacja ekranu nie powinna być blokowana przez długie obliczenia o niskim priorytecie.
  • W grach: renderowanie klatki jest ważniejsze niż generowanie mapy w tle.
  • W zadaniach krytycznych czasowo: sterowanie sprzętem, przetwarzanie sygnałów itp.

2. Priorytety wątków: jak to działa

W C# obsługa priorytetu wątku jest prosta — obiektowi Thread przypisujemy właściwość Priority. Przyjmuje ona wartości z enumeracji ThreadPriority:

Wartość Opis
Lowest
Najniższy priorytet
BelowNormal
Poniżej średniego
Normal
Zwykły (domyślny)
AboveNormal
Powyżej średniego
Highest
Najwyższy priorytet

Przykład kodu:

using System;
using System.Threading;

class Program
{
    static void Main()
    {
        Thread lowPriorityThread = new Thread(PrintLowPriority);
        Thread highPriorityThread = new Thread(PrintHighPriority);

        lowPriorityThread.Priority = ThreadPriority.Lowest;
        highPriorityThread.Priority = ThreadPriority.Highest;

        lowPriorityThread.Start();
        highPriorityThread.Start();
    }

    static void PrintLowPriority()
    {
        for (int i = 0; i < 10; i++)
        {
            Console.WriteLine("Niski priorytet: " + i);
            Thread.Sleep(10); // Dodajemy pauzę, żeby zobaczyć różnicę
        }
    }

    static void PrintHighPriority()
    {
        for (int i = 0; i < 10; i++)
        {
            Console.WriteLine("Wysoki priorytet: " + i);
            Thread.Sleep(10);
        }
    }
}

Do zapamiętania

W praktyce, zwłaszcza na nowoczesnych systemach wielordzeniowych i w zarządzanym środowisku .NET, priorytet to tylko wskazówka dla systemu operacyjnego. OS (i .NET) postarają się przydzielić więcej czasu wątkowi o wyższym priorytecie, ale nie ma tu żelaznych gwarancji. Na przykład jeśli twój wątek zabierze cały procesor z priorytetem Highest, interfejs może się zacząć przycinać, a pozostałe wątki będą cierpieć.

3. Zmiana priorytetów wątków

Wątki i priorytety

graph LR
    A[Wątek 1 - Lowest] -->|Mniej czasu procesora| OS(System operacyjny)
    B[Wątek 2 - Normal] --> OS
    C[Wątek 3 - Highest] -->|Więcej czasu procesora| OS
    OS --> CPU(Procesor)

Po co zmieniać priorytet wątku?

Może wydawać się, że można po prostu ustawić wszystkim Highest, ale to zły pomysł! Wyobraź sobie, że wszyscy klienci w sklepie jednocześnie będą krzyczeć "Obsłużcie mnie najpierw!".

Zmieniać priorytet tylko wtedy, gdy to uzasadnione:

  • Wątek zapisu logów — można ustawić BelowNormal lub Lowest.
  • Wątek obsługujący wejście użytkownika — AboveNormal.
  • Zadania CPU-intensywne, które nie są krytyczne czasowo — niski priorytet.

Lifehack: Jeśli aplikacja zaczyna się przycinać, sprawdź, czy nie masz "bezczelnych" wątków z wysokim priorytetem, które blokują resztę!

4. Typy (kategorie) wątków w .NET

W C# tradycyjnie wyróżnia się dwa typy wątków: foreground i background. Co ciekawe, "background" to niekoniecznie to samo, co "pracujący w tle". Zaraz wyjaśnimy.

Foreground-wątki (główne)

  • Domyślnie wszystkie tworzone przez ciebie wątki to foreground.
  • Dopóki w procesie żyje chociaż jeden foreground-wątek, aplikacja nie zakończy się, nawet jeśli wszystko inne "umarło".
  • Przykład: wątek główny programu (Main), oraz wszystkie wątki stworzone przez new Thread() bez dodatkowych zmian.

Background-wątki (pomocnicze)

  • Jeśli wszystkie foreground-wątki zakończą się, a pozostaną tylko background-wątki — proces po prostu się zamyka, a wątki są przerwane bez ostrzeżenia.
  • Są używane do zadań drugorzędnych: logowanie, wysyłka metryk w tle itp.
  • Aby zrobić wątek background: ustaw właściwość IsBackground na true:
Thread t = new Thread(SomeMethod);
t.IsBackground = true;
t.Start();

Demonstracja różnicy

using System;
using System.Threading;

class Program
{
    static void Main()
    {
        Thread backgroundThread = new Thread(() =>
        {
            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine("Background thread działa... " + i);
                Thread.Sleep(500);
            }
            Console.WriteLine("Background wątek ZAKOŃCZYŁ SIĘ.");
        });

        backgroundThread.IsBackground = true; // Robimy wątek background!

        backgroundThread.Start();

        Console.WriteLine("Main kończy pracę za 1 sekundę.");
        Thread.Sleep(1000); // Czekamy sekundę
        Console.WriteLine("Main zakończył się. Co stanie się z background-wątkiem?");
    }
}

Co zobaczysz?
Main może się zakończyć, a background-wątek "wyłączy się" w połowie. To wygodne dla zadań, które nie powinny blokować zamknięcia aplikacji.

5. Rodzaje wątków

ThreadPool (pula wątków)

  • ThreadPool — mechanizm zarządzający pulą wątków, żeby nie tworzyć ich zbyt wielu.
  • Używa się go, gdy zadań jest dużo i są krótkie: równoległe przetwarzanie żądań, operacje asynchroniczne.
  • Wątki z puli zawsze są background — nie przeszkodzą w zamknięciu aplikacji.

Przykład: uruchomienie kodu w puli wątków

using System;
using System.Threading;

class Program
{
    static void Main()
    {
        ThreadPool.QueueUserWorkItem(DoWorkInThreadPool, "zadanie w tle");
        Console.WriteLine("Main kończy pracę.");
        Thread.Sleep(500);
    }

    static void DoWorkInThreadPool(object? state)
    {
        Console.WriteLine("Wątek z puli: " + state);
        Thread.Sleep(1000); // Spróbujemy zasnąć
        Console.WriteLine("Wątek z puli zakończył się!");
    }
}

Na co zwrócić uwagę:
Gdyby Main zakończył się wcześniej, wątek z puli mógłby zostać przerwany. Jeśli potrzebujesz gwarancji zakończenia — użyj foreground-wątków lub czekaj explicite.

Ewolucja wielowątkowości: Task, async/await

W nowoczesnych aplikacjach rzadko tworzy się ręcznie wątki (Thread). Zwykle korzysta się z Task i metod asynchronicznych. Warto wiedzieć:

  • Wątki z puli i zadania Task działają na background-wątkach.
  • Dla większości zadań nie trzeba zmieniać priorytetu — trzymaj się zdrowego rozsądku i najlepszych praktyk.

6. Przydatne niuanse

Właściwości i zachowanie wątków

Właściwość Foreground Thread Background Thread ThreadPool Thread
IsBackground
false domyślnie true true
Zakończenie aplikacji Aplikacja NIE zakończy się, dopóki żyje przynajmniej jeden foreground-wątek Aplikacja zakończy się, jeśli wszystkie foreground-wątki się zakończą Aplikacja zakończy się, gdy Main się zakończy
Zarządzanie Pełna kontrola Pełna kontrola Brak bezpośredniej kontroli
Gdzie używane Długie, ważne zadania (np. serwer DB) Nieistotne zadania, operacje w tle, logowanie Krótkie zadania, Task, operacje asynchroniczne
Priorytet Można ustawiać Można ustawiać Nie zaleca się zmieniać

Niejoczywiste triki i uwagi

  • Priorytet można zmieniać tylko dla zwykłych wątków (Thread), a nie dla zadań z puli (Task, ThreadPool).
  • Wątki z puli zawsze mają priorytet Normal (nie można zmieniać).
  • Task i async/await — to nowocześniejsze podejście, w którym kwestie priorytetów i działania w tle są ukryte "za kulisami".

7. Typowe błędy związane z priorytetami i typami wątków

Błąd #1: nadużywanie priorytetów.
Ustawianie wszystkim wątkom Highest lub wszystkim Lowest bez obiektywnego powodu nie daje korzyści. Może to zaburzyć balans wykonywania zadań i obniżyć responsywność aplikacji.

Błąd #2: niejawne zakończenie background-wątków.
Jeśli ważna praca (np. zapis danych) wykonywana jest w background-wątku i nie czekasz na jej zakończenie, ryzykujesz utratę danych. Background-wątki są automatycznie przerywane przy zakończeniu procesu.

Błąd #3: zawyżone oczekiwania wobec priorytetu wątku.
Priorytet wątku (Priority) to tylko rekomendacja dla OS, a nie gwarancja, że wątek zostanie wykonany jako pierwszy.

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