JavaRush /Курсы /C# SELF /Исключения в классических ...

Исключения в классических 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: отсутствие логирования.
Всегда логируйте ошибки в потоках, даже если кажется, что «ничего страшного» не произойдёт.

2
Задача
C# SELF, 61 уровень, 3 лекция
Недоступна
Глобальный обработчик необработанных исключений
Глобальный обработчик необработанных исключений
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ