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 |
|---|---|
|
Najniższy priorytet |
|
Poniżej średniego |
|
Zwykły (domyślny) |
|
Powyżej średniego |
|
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 |
|---|---|---|---|
|
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.
GO TO FULL VERSION