1. Вступ
Коли ми працюємо з класом Task або з асинхронними методами (async/await), можемо ловити винятки звичним чином — через try-catch навколо await або за допомогою ContinueWith. Винятки не «втрачаються», а повертаються у потік, що їх очікує.
Але якщо ми створюємо потік через Thread, усе стає складніше. У кожного потоку своя точка входу (ThreadStart) і контекст виконання. Якщо всередині потоку станеться необроблений виняток, він не «повернеться» в основний потік — його буде кинуто лише в цей потік.
- У .NET Framework: необроблений виняток в одному потоці завершує весь застосунок.
- У .NET (Core/5+): завершиться лише цей потік, застосунок продовжить працювати (що може призвести до прихованих багів).
Підсумок: якщо не перехопити винятки всередині потоку, ви їх, найімовірніше, просто не побачите. Тому грамотна обробка помилок у потоках — необхідність.
Цікавий факт: винятки, що вислизнули з Thread, схожі на невловимого ніндзя: вони зникають, а ви потім ламаєте голову, чому логіка не спрацювала.
2. Як працюють винятки всередині потоку?
using System;
using System.Threading;
class Program
{
static void Main()
{
Thread thread = new Thread(DoWork);
thread.Start();
// Дочекаймося завершення потоку, щоб побачити, що станеться
thread.Join();
Console.WriteLine("Main завершився нормально");
}
static void DoWork()
{
Console.WriteLine("Працюємо в окремому потоці...");
throw new Exception("Біда! У потоці сталася помилка.");
}
}
Залежно від платформи (стара .NET Framework чи сучасний .NET Core/5/6/7/8/9), поведінка буде різною: або впаде весь застосунок, або лише потік. Але головне — виняток не потрапить до основного потоку, і ви не зможете обробити його ззовні.
Важливо! Спроба обгорнути thread.Join() у try-catch не допоможе спіймати виняток з іншого потоку — він «живе» і «вмирає» всередині нього.
3. Як правильно ловити винятки в Thread?
Лише всередині потоку — у тій функції, яку ви передаєте в конструктор Thread. Усе, що може кидати помилки, обгорніть у try-catch.
static void DoWork()
{
try
{
Console.WriteLine("Працюємо...");
throw new Exception("Знову щось пішло не так!");
}
catch (Exception ex)
{
Console.WriteLine($"[Потік] Спіймали виняток: {ex.Message}");
// Тут можна записати до журналу, надіслати до UI або на сервер тощо.
}
}
Обробка винятків у потоках — це відповідальність коду потоку. Розраховувати, що код, який викликає, автоматично перехопить помилку, не варто.
4. Як дізнатися в головному потоці, що в іншому щось пішло не так?
У реальних застосунках важливо доставити інформацію про помилку в головний потік.
- Використовуйте потокобезпечні механізми, наприклад ConcurrentQueue<Exception>, щоб передавати винятки з потоків.
- Ініціюйте події або делегати з робочого потоку.
- Надавайте перевагу Task, який «з коробки» доставить виняток до місця виклику await.
Приклад: збираємо інформацію про помилку в спеціальне місце
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($"В іншому потоці сталася помилка: {threadException.Message}");
}
else
{
Console.WriteLine("Потік завершився без помилок.");
}
}
static void DoWork()
{
try
{
throw new Exception("Біда в іншому потоці!");
}
catch (Exception ex)
{
threadException = ex;
}
}
}
Зауваження: такий підхід годиться при синхронному очікуванні (Join()). Якщо потік «живе своїм життям» або помилок багато — використовуйте ConcurrentQueue<Exception>, події або інші механізми комунікації.
5. Порівняння з Task: чому підхід до обробки помилок простіший
async Task FooAsync()
{
throw new Exception("Помилка в задачі!");
}
try
{
await FooAsync();
}
catch (Exception ex)
{
Console.WriteLine($"Спіймали помилку: {ex.Message}");
}
Тут усе прозоро: помилка «доїжджає» до місця, де ви очікуєте її через await. З класичним Thread помилка лишається всередині потоку і не передається нагору без спеціальних дій. Це один із аргументів на користь використання Task та сучасних абстракцій.
6. Практичний приклад
В UI‑застосунках (WPF/WinForms) потоки використовують, щоб не блокувати інтерфейс. Винятки без обробки призводять до «сірого екрана» й дивних зависань.
Погано (потік без обробки помилок)
Thread thread = new Thread(() =>
{
// Довго думаємо
Thread.Sleep(5000);
throw new Exception("Усе пропало!"); // ніхто не спіймає
});
thread.Start();
Добре (ловимо помилку і повідомляємо користувача)
Thread thread = new Thread(() =>
{
try
{
Thread.Sleep(5000);
throw new Exception("Щось не так");
}
catch (Exception ex)
{
// Можна показати MessageBox, записати до журналу або передати до UI
Console.WriteLine($"Помилка в потоці: {ex.Message}");
}
});
thread.Start();
7. Корисні нюанси
Глобальний перехоплювач для необроблених винятків потоку
AppDomain.CurrentDomain.UnhandledException += (sender, args) =>
{
Console.WriteLine($"Глобално спіймали помилку: {((Exception)args.ExceptionObject).Message}");
};
Thread thread = new Thread(() =>
{
throw new Exception("Екстермінатус!");
});
thread.Start();
Обробник AppDomain.CurrentDomain.UnhandledException спрацьовує для необроблених винятків у потоках, але не дозволяє «воскресити» потік або запобігти завершенню процесу в .NET Framework. У .NET (Core/5+) він логуватиме помилку; застосунок може продовжити роботу, якщо інші потоки активні.
Відмінності в обробці винятків — Thread vs Task
|
|
|
|---|---|---|
| Де ловити | Всередині потоку | У коді, що викликає (await, ContinueWith, тощо) |
| Наслідки | Виняток губиться або завершує потік (або весь застосунок у .NET Framework) | Виняток доїжджає до місця очікування (await) |
| Повідомлення нагору | Лише явно (змінні, події, черги) | Через await, AggregateException при синхронному очікуванні |
| Логування | Потрібно робити вручну в коді потоку | Зазвичай у try-catch навколо await |
| Контекст | Незалежний від батьківського потоку | Task використовує контекст синхронізації коду, що викликає (наприклад, UI‑контекст у WPF) |
8. Типові помилки під час роботи з винятками в Thread
Помилка № 1: не ловлять винятки всередині потоку.
У підсумку можна отримати неявне завершення частини застосунку, а інколи й усього процесу, причому без зрозумілої діагностики.
Помилка № 2: намагаються «спіймати» виняток із потоку в головному потоці.
Це не працює: try-catch навколо thread.Join() або thread.Start() не перехопить помилку, кину ту всередині потоку.
Помилка № 3: втрачають інформацію про помилку.
Якщо потік упав, а ви не передали виняток явно (змінна, черга, подія), ви не дізнаєтеся ні причину, ні деталі. Це веде до «примарних» багів.
Помилка № 4: відсутність логування.
Завжди записуйте помилки в потоках до журналу, навіть якщо здається, що «нічого страшного» не станеться.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ