JavaRush /Курси /C# SELF /Винятки в класичних Thread...

Винятки в класичних Thread

C# SELF
Рівень 61 , Лекція 3
Відкрита

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

Thread
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: відсутність логування.
Завжди записуйте помилки в потоках до журналу, навіть якщо здається, що «нічого страшного» не станеться.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ