1. Wprowadzenie
Kiedy pracujemy z klasą Task albo asynchronicznymi metodami (async/await), możemy łapać wyjątki w zwykły sposób — przez try-catch wokół await albo za pomocą ContinueWith. Wyjątki nie „gubią się”, tylko wracają do wywołującego wątku.
Ale jeśli tworzymy wątek przez Thread, robi się trudniej. Każdy wątek ma własny punkt wejścia (ThreadStart) i kontekst wykonania. Jeśli wewnątrz wątku wystąpi nieobsłużony wyjątek, on nie „wróci” do głównego wątku — zostanie rzucony tylko w tym wątku.
- W .NET Framework: nieobsłużony wyjątek w jednym wątku kończy całą aplikację.
- W .NET (Core/5+): zakończy się tylko ten wątek, aplikacja będzie dalej działać (co może prowadzić do ukrytych bugów).
Wniosek: jeśli nie przechwycisz wyjątków wewnątrz wątku, najpewniej ich po prostu nie zobaczysz. Dlatego porządna obsługa błędów w wątkach to konieczność.
Ciekawostka: wyjątki, które wylecą z Thread, są jak nieuchwytny ninja: znikają, a potem łamiesz sobie głowę, dlaczego logika nie zadziałała.
2. Jak działają wyjątki wewnątrz wątku?
using System;
using System.Threading;
class Program
{
static void Main()
{
Thread thread = new Thread(DoWork);
thread.Start();
// Poczekamy na zakończenie wątku, żeby zobaczyć, co się stanie
thread.Join();
Console.WriteLine("Main zakończył się poprawnie");
}
static void DoWork()
{
Console.WriteLine("Pracujemy w oddzielnym wątku...");
throw new Exception("Katastrofa! W wątku wystąpił błąd.");
}
}
W zależności od platformy (stary .NET Framework albo nowoczesny .NET Core/5/6/7/8/9) zachowanie będzie różne: albo spadnie cała aplikacja, albo tylko wątek. Ale najważniejsze — wyjątek nie przyjdzie do głównego wątku i nie będziesz w stanie go obsłużyć z zewnątrz.
Uwaga! Próba owinięcia thread.Join() w try-catch nie pomoże w złapaniu wyjątku z innego wątku — on „żyje” i „umiera” wewnątrz niego.
3. Jak trzeba łapać wyjątki w Thread?
Tylko wewnątrz wątku — w tej funkcji, którą przekazujesz do konstruktora Thread. Wszystko, co może rzucać błędy, owijaj w try-catch.
static void DoWork()
{
try
{
Console.WriteLine("Pracujemy...");
throw new Exception("Znowu coś poszło nie tak!");
}
catch (Exception ex)
{
Console.WriteLine($"[Wątek] Złapano wyjątek: {ex.Message}");
// Tutaj możesz zalogować, wysłać do UI/na serwer itp.
}
}
Obsługa wyjątków w wątkach to odpowiedzialność kodu wątku. Nie można liczyć, że kod wywołujący automatycznie złapie błąd.
4. Jak dowiedzieć się w głównym wątku, że w innym coś poszło nie tak?
W realnych aplikacjach ważne jest dostarczenie informacji o błędzie do głównego wątku.
- Użyj mechanizmów bezpiecznych dla wątków, np. ConcurrentQueue<Exception>, żeby przekazywać wyjątki z wątków.
- Rzuć zdarzenia/delegaty z wątku roboczego.
- Wol preferować Task, który dostarczy wyjątek do miejsca wywołania await „z pudełka”.
Przykład: zbieramy błąd w specjalne miejsce
using System;
using System.Threading;
class Program
{
static Exception? threadException = null;
static void Main()
{
Thread thread = new Thread(DoWork);
thread.Start();
thread.Join();
if (threadException != null)
{
Console.WriteLine($"W innym wątku wystąpił błąd: {threadException.Message}");
}
else
{
Console.WriteLine("Wątek zakończył się bez błędów.");
}
}
static void DoWork()
{
try
{
throw new Exception("Kłopoty w innym wątku!");
}
catch (Exception ex)
{
threadException = ex;
}
}
}
Uwaga: takie podejście nadaje się przy synchronicznym oczekiwaniu (Join()). Jeśli wątek „żyje swoim życiem” albo błędów jest dużo — użyj ConcurrentQueue<Exception>, zdarzeń albo innych mechanizmów komunikacji.
5. Porównanie z Task: dlaczego obsługa błędów jest prostsza
async Task FooAsync()
{
throw new Exception("Błąd w zadaniu!");
}
try
{
await FooAsync();
}
catch (Exception ex)
{
Console.WriteLine($"Złapano błąd: {ex.Message}");
}
Tutaj wszystko jest przejrzyste: błąd „dojeżdża” do miejsca, w którym go await-ujesz. W klasycznym Thread błąd zostaje wewnątrz wątku i nie przekazuje się w górę bez specjalnych działań. To jeden z argumentów za używaniem Task i nowoczesnych abstrakcji.
6. Praktyczny przykład
W aplikacjach UI (WPF/WinForms) wątki używane są, żeby nie blokować interfejsu. Nieobsługiwane wyjątki prowadzą do „szarego ekranu” i dziwnych zacięć.
Źle (wątek bez obsługi błędów)
Thread thread = new Thread(() =>
{
// Długo myślimy
Thread.Sleep(5000);
throw new Exception("Wszystko przepadło!"); // nikt nie złapie
});
thread.Start();
Dobrze (łapiemy błąd i informujemy użytkownika)
Thread thread = new Thread(() =>
{
try
{
Thread.Sleep(5000);
throw new Exception("Coś poszło nie tak");
}
catch (Exception ex)
{
// Można pokazać MessageBox, zalogować lub przesłać do UI
Console.WriteLine($"Błąd w wątku: {ex.Message}");
}
});
thread.Start();
7. Przydatne niuanse
Globalny hook dla nieobsłużonych wyjątków w wątku
AppDomain.CurrentDomain.UnhandledException += (sender, args) =>
{
Console.WriteLine($"Globalnie złapano błąd: {((Exception)args.ExceptionObject).Message}");
};
Thread thread = new Thread(() =>
{
throw new Exception("Eksterminatus!");
});
thread.Start();
Handler AppDomain.CurrentDomain.UnhandledException uruchamia się dla nieobsłużonych wyjątków w wątkach, ale nie pozwala „wskrzeszać” wątku ani zapobiec zakończeniu procesu w .NET Framework. W .NET (Core/5+) loguje błąd; aplikacja może dalej działać, jeśli pozostałe wątki są aktywne.
Różnice w obsłudze wyjątków — Thread vs Task
|
|
|
|---|---|---|
| Gdzie łapać | Wewnątrz wątku | W kodzie wywołującym (await, ContinueWith, itd.) |
| Konsekwencje | Wyjątek ginie/zabija wątek (albo całą aplikację w .NET Framework) | Wyjątek dociera do miejsca oczekiwania (await) |
| Powiadomienie w górę | Tylko jawnie (zmienne, zdarzenia, kolejki) | Przez await, AggregateException przy synchronicznym oczekiwaniu |
| Logowanie | Trzeba robić ręcznie w kodzie wątku | Zwykle w try-catch wokół await |
| Kontekst | Niezależny od wątku rodzica | Task używa kontekstu synchronizacji wywołującego (np. UI-kontekst w WPF) |
8. Typowe błędy przy pracy z wyjątkami w Thread
Błąd nr 1: nie łapią wyjątków wewnątrz wątku.
W efekcie możesz dostać niejawne zakończenie części aplikacji, a czasem i całego procesu, często bez czytelnej diagnostyki.
Błąd nr 2: próbują „złapać” wyjątek z wątku w głównym wątku.
To nie działa: try-catch wokół thread.Join() lub thread.Start() nie przechwyci błędu rzuconego wewnątrz wątku.
Błąd nr 3: gubią informacje o błędzie.
Jeśli wątek upadł, a nie przekazałeś wyjątku jawnie (zmienna, kolejka, zdarzenie), nie dowiesz się ani przyczyny, ani szczegółów. To prowadzi do „duchowych” bugów.
Błąd nr 4: brak logowania.
Zawsze loguj błędy w wątkach, nawet jeśli wydaje się, że „nie stanie się nic strasznego”.
GO TO FULL VERSION