CodeGym /课程 /C# SELF /经典 Thread 中的异常

经典 Thread 中的异常

C# SELF
第 61 级, 课程 3
可用

1. 介绍

当我们使用 Task 类或异步方法(async/await)时,我们可以像平常那样捕获异常——在 await 周围用 try-catch,或者用 ContinueWith。异常不会“丢失”,会返回到调用线程。

但如果我们通过 Thread 创建线程,情况就复杂了。每个线程都有自己的入口点(ThreadStart)和执行上下文。如果线程内部发生未处理的异常,它不会“返回”到主线程——它只会在该线程中抛出。

  • .NET Framework:一个线程中的未处理异常会终止整个应用程序。
  • .NET (Core/5+):只有该线程会终止,应用程序会继续运行(这可能导致隐蔽的 bug)。

结论:如果不在线程内部捕获异常,你大概率看不到它们。因此在线程中做好错误处理是必须的。

有趣的事实:从 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
记录日志 需要在线程代码中手动记录 通常在包裹 await 的 try-catch 中记录
上下文 独立于父线程的上下文 Task 使用调用方的同步上下文(例如 WPF 的 UI 上下文)

8. 使用 Thread 时处理异常的常见错误

错误 №1:在线程内部不捕获异常。
结果可能是应用的一部分隐式结束,甚至整个进程崩溃,而且没有清晰的诊断信息。

错误 №2:试图在主线程捕获另一个线程的异常。
这不行:把 try-catch 放在 thread.Join() 或 thread.Start() 外面不会捕获线程内部抛出的异常。

错误 №3:丢失错误信息。
如果线程崩了,但你没有显式传递异常(变量、队列、事件),你既不知道原因也不知道细节。这会带来“幽灵” bug。

错误 №4:没有日志。
一定在线程中记录错误,即便看起来“没什么大不了”。

2
任务
C# SELF, 第 61 级, 课程 3
已锁定
全局未处理异常处理器
全局未处理异常处理器
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION