1. Wprowadzenie
Wyobraź sobie, że wątek to niewyczerpany pracownik, któremu zlecasz zadanie. Pracownik może spać (jeszcze nie zaczął), harować przy zadaniu (wątek wykonuje metodę), czekać na nową instrukcję (bezczynność), albo skończyć pracę (zakończony).
W C# (i ogólnie w .NET) cykl życia wątku składa się z kilku stanów:
- Unstarted — wątek utworzony, ale jeszcze nie uruchomiony.
- Running — wątek wykonuje kod.
- WaitSleepJoin — wątek tymczasowo nie działa (np. oczekuje sygnału lub "śpi").
- Stopped — wątek wykonał zadanie i zakończył się.
Można to zobrazować takim schematem:
stateDiagram-v2
[*] --> Unstarted
Unstarted --> Running: Start()
Running --> WaitSleepJoin: Wait/Sleep/Join
WaitSleepJoin --> Running: Otrzymano sygnał/Czas minął
Running --> Stopped: Metoda zakończona
WaitSleepJoin --> Stopped: Metoda zakończona
Stopped --> [*]
Wszystko zaczyna się od utworzenia obiektu Thread, ale dopóki nie wywołasz Start(), wątek "drzemie" w stanie Unstarted. Po Start() — zaczyna się zabawa, wątek przechodzi do Running. Jeśli wątek w środku wywoła Thread.Sleep lub będzie czegoś oczekiwał (np. Monitor.Wait), trafi do specjalnego stanu oczekiwania. Kiedy metoda przypisana wątkowi kończy działanie, wątek "umiera" i nie "wstanie" ponownie. Koniec — bilet w jedną stronę.
2. Praktyka: Cykl życia prostego wątku
Spójrzmy na klasyczny przykład:
using System;
using System.Threading;
class Program
{
static void Main()
{
// Tworzymy wątek — na razie planujemy pracę
Thread worker = new Thread(DoWork);
Console.WriteLine($"Status wątku po utworzeniu: {worker.ThreadState}");
// Uruchamiamy wątek
worker.Start();
Console.WriteLine($"Status wątku po starcie: {worker.ThreadState}");
// Dajemy głównemu wątkowi pospać, żeby worker zdążył popracować
Thread.Sleep(100);
Console.WriteLine($"Status wątku (później): {worker.ThreadState}");
// Czekamy, aż worker się zakończy (dołączamy)
worker.Join();
Console.WriteLine($"Status wątku po zakończeniu: {worker.ThreadState}");
Console.WriteLine("Główny wątek zakończony");
}
static void DoWork()
{
Console.WriteLine("Wątek roboczy zaczął pracę!");
Thread.Sleep(500);
Console.WriteLine("Wątek roboczy zakończył pracę!");
}
}
Co wypisze program?
- Po utworzeniu wątku — status będzie Unstarted.
- Po starcie — zwykle od razu Running (choć może być też Running | Background).
- W trakcie pracy — status może być Running, albo WaitSleepJoin, jeśli wątek "śpi".
- Po zakończeniu metody — status staje się Stopped.
Ten kod to świetne narzędzie do zrozumienia, w jakim stanie może być wątek. Możesz pokombinować z opóźnieniami i obserwować zmiany stanu.
3. Sterowanie wątkiem: podstawowe metody
Start: Start()
To oczywiste, ale powtórzmy: tworzysz wątek — uruchamiasz go metodą Start(). Uruchamiać można tylko raz: próba wywołania Start() ponownie spowoduje wyjątek ThreadStateException.
Thread t = new Thread(MyMethod);
t.Start(); // OK
t.Start(); // Błąd!
Oczekiwanie na zakończenie: Join()
Czasem trzeba poczekać, aż wątek skończy, i dopiero wtedy kontynuować. Do tego służy Join().
Thread t = new Thread(MyMethod);
t.Start();
t.Join(); // Blokuje bieżący wątek do zakończenia t
Jeśli masz kilka wątków, możesz wywołać Join() dla każdego — wątek główny poczeka, aż wszyscy "pracusie" skończą.
Warianty: jest przeciążenie Join(int millisecondsTimeout), które czeka tylko określony czas, a potem kontynuuje.
// Czekamy maksymalnie 2 sekundy
if (t.Join(2000))
Console.WriteLine("Wątek zakończył się na czas");
else
Console.WriteLine("Czekanie się znudziło...");
Przymusowe zatrzymanie: dlaczego to zły pomysł
W starych wersjach .NET był method Thread.Abort(), który pozwalał "zabić" wątek natychmiast. Dziś raczej się go nie używa — jest niebezpieczny i może zostawić program w dziwnym stanie. Filozofia .NET jest taka: wątek powinien się zakończyć dobrowolnie. Nie "zabijasz" pracownika — dajesz mu znać, że dzień pracy się skończył.
4. Jak poprawnie "zatrzymać" wątek
Najlepszy i najbezpieczniejszy sposób zatrzymania wątku to użycie flagi anulowania lub znacznika zakończenia, który wątek okresowo sprawdza.
class Worker
{
private volatile bool shouldStop = false;
public void DoWork()
{
while (!shouldStop)
{
Console.WriteLine("Pracuję!");
Thread.Sleep(300);
}
Console.WriteLine("Wątek kończy pracę na polecenie.");
}
public void RequestStop()
{
shouldStop = true;
}
}
Użycie:
Worker w = new Worker();
Thread t = new Thread(w.DoWork);
t.Start();
// Czekamy chwilę
Thread.Sleep(1000);
// Prosimy wątek o zakończenie
w.RequestStop();
t.Join(); // Czekamy na zakończenie wątku
Ważna uwaga: volatile
Słowo kluczowe volatile mówi kompilatorowi i procesorowi: "Nie cachuj tego pola, zawsze czytaj aktualną wartość!" To ważne, żeby wątek zobaczył świeży znak zatrzymania. Bez tego (albo bez innych technik synchronizacji) wątek może nigdy nie zauważyć twoich zmian.
5. Przejścia wątku do stanów oczekiwania i snu
Czasami wątek tymczasowo nic nie robi — albo śpi, albo oczekuje.
Sen: Thread.Sleep
Gdy chcesz dać wątkowi odpocząć albo spowolnić jego pracę (np. żeby nie obciążać CPU), użyj Thread.Sleep(milliseconds).
// Wątek śpi 2 sekundy
Thread.Sleep(2000);
W czasie snu wątek nic nie robi.
Oczekiwanie / Join
Gdy wątek główny czeka, aż potomny się zakończy (Join), główny jest "w pauzie". Podobnie, jeśli wątek czeka na zwolnienie zasobu (np. przy pomocy monitorów lub innych prymitywów synchronizacji), przechodzi w specjalny stan oczekiwania.
6. Kontrola background/foreground wątku
W .NET wątki są dwojakiego rodzaju: foreground (pierwszoplanowe) i background (tła). Różnica jest prosta:
- Jeśli w procesie pozostają tylko wątki tła, proces zakończy się automatycznie.
- Główny wątek i wszystkie wątki pierwszoplanowe muszą się zakończyć, żeby proces przestał działać.
Możesz jawnie ustawić wątek jako tło:
Thread t = new Thread(SomeMethod);
t.IsBackground = true; // Ustawiamy jako tło
t.Start();
Praktyczny przykład — Demon vs. Zwykły wątek
Thread t = new Thread(() =>
{
while (true)
{
Console.WriteLine("Jestem duchem (tło), nie da się mnie zatrzymać!");
Thread.Sleep(500);
}
});
t.IsBackground = true; // Robimy wątek tłem
t.Start();
Thread.Sleep(1200);
Console.WriteLine("Główny wątek kończy pracę");
// Po zakończeniu Main — proces ginie, i nasz wieczny wątek też znika
Po zakończeniu Main — proces się kończy; wątki tła zostają przerwane automatycznie.
7. Przydatne niuanse
Czego nie robić z wątkami
- Nie można "zrestartować" wątku. Obiekt Thread żyje jednokrotnie: gdy metoda się zakończy — wątek umiera, a próba wywołania Start() ponownie zakończy się błędem.
- Nie należy na siłę zatrzymywać cudzych wątków metodami Thread.Abort() czy Thread.Suspend() — to przestarzałe i niebezpieczne.
- Nie ignoruj zakończenia wątku. Jeśli wątek pracuje z plikami lub zasobami, poprawnie je zwalniaj przed końcem wątku.
Sprawdzanie stanu i kontrola cyklu życia
if (t.IsAlive)
{
Console.WriteLine("Wątek wciąż żyje");
}
else
{
Console.WriteLine("Wątek się zakończył");
}
IsAlive — true, dopóki wątek wykonuje swoją metodę; po zakończeniu — false.
Cykl życia prostego wątku w .NET
| Stan | Jak wejść | Co to znaczy? | Jak wyjść |
|---|---|---|---|
| Unstarted | |
Wątek utworzony, nieuruchomiony | Wywołać Start() |
| Running | |
Wątek wykonuje pracę | Zakończyć metodę |
| WaitSleepJoin | Sleep(), Join(), oczekiwanie | Wątek tymczasowo nieaktywny | Oczekiwanie się kończy |
| Stopped | Metoda wątku zakończona | Wątek "umarł" | Nie wyjdzie — koniec |
FAQ dotyczące zarządzania życiem wątku
Pytanie: Czy można zabić wątek na rozkaz?
Odpowiedź: Nie i nie powinno się. Wątki powinny same pilnować zakończenia pracy. Używaj flag anulowania.
Pytanie: Czy można ponownie użyć obiektu Thread?
Odpowiedź: Nie. Twórz nowy obiekt dla nowego zadania.
Pytanie: Co się stanie, jeśli główny wątek się zakończy, a podrzędny będzie dalej działał?
Odpowiedź: Jeśli podrzędny jest wątkiem tła (IsBackground == true), aplikacja się zakończy. Jeśli nie — proces będzie żył, dopóki wszystkie wątki się nie zamkną.
Pytanie: Jak poprawnie sprzątać zasoby, jeśli wątek kończy pracę z powodu anulowania?
Odpowiedź: Używaj bloków try...finally wewnątrz metody wątku, żeby zasoby były zwalniane w każdym przypadku.
8. Typowe błędy i jak ich unikać przy pracy z wątkami
Błąd #1: ponowne użycie tego samego obiektu Thread.
Nie można uruchomić tego samego obiektu wątku więcej niż raz. Po zakończeniu wątku nie da się go "zrestartować" — spowoduje to wyjątek.
Błąd #2: niepoprawne zwalnianie zewnętrznych zasobów w wątku.
Jeśli wątek pracuje z plikami, siecią lub innymi zasobami, koniecznie zapewnij ich poprawne zamknięcie i zwolnienie. Zalecane są bloki finally lub konstrukcje using, żeby uniknąć wycieków i blokad.
Błąd #3: tworzenie zbyt wielu wątków.
Nadmierna liczba wątków utrudnia debugowanie i może pogorszyć wydajność. Jeden zbędny wątek to dodatkowy czas na szukanie i naprawę niespodziewanych bugów.
GO TO FULL VERSION