CodeGym /Các khóa học /C# SELF /Ngoại lệ trong Thread

Ngoại lệ trong Thread cổ điển

C# SELF
Mức độ , Bài học
Có sẵn

1. Giới thiệu

Khi chúng ta làm việc với lớp Task hoặc phương thức bất đồng bộ (async/await), chúng ta có thể bắt ngoại lệ theo cách quen thuộc — qua try-catch quanh await hoặc bằng ContinueWith. Ngoại lệ không "bị mất", mà được trả về cho luồng gọi.

Nhưng nếu ta tạo luồng bằng Thread, mọi thứ phức tạp hơn. Mỗi luồng có điểm vào riêng (ThreadStart) và ngữ cảnh thực thi riêng. Nếu trong luồng xảy ra ngoại lệ chưa được xử lý, nó sẽ không "trở về" luồng chính — nó chỉ bị ném trong chính luồng đó.

  • Trong .NET Framework: ngoại lệ không xử lý trong một luồng sẽ kết thúc toàn bộ ứng dụng.
  • Trong .NET (Core/5+): chỉ có luồng đó kết thúc, ứng dụng tiếp tục chạy (điều này có thể dẫn đến bug ẩn).

Kết luận: nếu không bắt ngoại lệ bên trong luồng, rất có khả năng bạn sẽ không thấy chúng. Vì vậy xử lý lỗi đúng cách trong luồng là cần thiết.

Sự thật thú vị: ngoại lệ bay mất từ Thread giống như ninja vô hình: nó biến mất, và bạn sau đó tự hỏi vì sao logic không chạy.

2. Ngoại lệ hoạt động thế nào bên trong luồng?

using System;
using System.Threading;

class Program
{
    static void Main()
    {
        Thread thread = new Thread(DoWork);
        thread.Start();

        // Chờ luồng kết thúc để xem chuyện gì xảy ra
        thread.Join();

        Console.WriteLine("Main kết thúc bình thường");
    }

    static void DoWork()
    {
        Console.WriteLine("Làm việc trong luồng riêng...");
        throw new Exception("Tai họa! Trong luồng xảy ra lỗi.");
    }
}

Tùy nền tảng (.NET Framework cũ hay .NET Core/5/6/7/8/9 hiện đại), hành vi khác nhau: hoặc cả ứng dụng sập, hoặc chỉ luồng bị dừng. Nhưng điều chính — ngoại lệ sẽ không đến luồng chính, và bạn không thể xử lý nó từ bên ngoài.

Quan trọng! Cố gắng bọc thread.Join() bằng try-catch sẽ không giúp bắt ngoại lệ từ luồng khác — nó "sống" và "chết" bên trong luồng đó.

3. Cách bắt ngoại lệ trong Thread?

Chỉ bên trong luồng — trong hàm bạn truyền vào constructor của Thread. Mọi thứ có thể ném lỗi thì hãy bọc bằng try-catch.

static void DoWork()
{
    try
    {
        Console.WriteLine("Đang làm việc...");
        throw new Exception("Lại có chuyện gì đó không ổn!");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"[Luồng] Bắt được ngoại lệ: {ex.Message}");
        // Ở đây có thể log, gửi lên UI/server v.v.
    }
}

Xử lý ngoại lệ trong luồng là trách nhiệm của mã luồng. Không thể mong đợi mã gọi sẽ tự động bắt lỗi.

4. Làm sao biết ở luồng chính rằng luồng khác có lỗi?

Trong ứng dụng thực tế quan trọng là chuyển thông tin lỗi về luồng chính.

  • Sử dụng cơ chế thread-safe, ví dụ ConcurrentQueue<Exception>, để truyền ngoại lệ từ các luồng.
  • Nâng sự kiện/delegate từ luồng làm việc.
  • Ưu tiên dùng Task, nó sẽ đưa ngoại lệ tới chỗ gọi await "mặc định".

Ví dụ: gom lỗi vào nơi đặc biệt

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($"Trong luồng khác xảy ra lỗi: {threadException.Message}");
        }
        else
        {
            Console.WriteLine("Luồng kết thúc không lỗi.");
        }
    }

    static void DoWork()
    {
        try
        {
            throw new Exception("Tai họa trong luồng khác!");
        }
        catch (Exception ex)
        {
            threadException = ex;
        }
    }
}

Ghi chú: cách này phù hợp khi chờ đồng bộ (Join()). Nếu luồng "sống riêng" hoặc lỗi nhiều — dùng ConcurrentQueue<Exception>, sự kiện hoặc cơ chế giao tiếp khác.

5. So sánh với Task: tại sao xử lý lỗi đơn giản hơn

async Task FooAsync()
{
    throw new Exception("Lỗi trong task!");
}

try
{
    await FooAsync();
}
catch (Exception ex)
{
    Console.WriteLine($"Bắt được lỗi: {ex.Message}");
}

Ở đây mọi thứ minh bạch: lỗi "đi" tới chỗ bạn await nó. Với Thread cổ điển lỗi ở lại trong luồng và không được truyền lên trên nếu không có hành động đặc biệt. Đây là một trong những lý do nên dùng Task và các trừu tượng hiện đại.

6. Ví dụ thực tế

Trong ứng dụng UI (WPF/WinForms) luồng được dùng để không block giao diện. Ngoại lệ không xử lý dẫn đến "màn hình xám" và treo khó hiểu.

Tệ (luồng không bắt lỗi)

Thread thread = new Thread(() =>
{
    // Nghĩ lâu
    Thread.Sleep(5000);
    throw new Exception("Mọi thứ sụp đổ!"); // không ai bắt
});
thread.Start();

Tốt (bắt lỗi và thông báo người dùng)

Thread thread = new Thread(() =>
{
    try
    {
        Thread.Sleep(5000);
        throw new Exception("Có chuyện không ổn");
    }
    catch (Exception ex)
    {
        // Có thể show MessageBox, log hoặc gửi về UI
        Console.WriteLine($"Lỗi trong luồng: {ex.Message}");
    }
});
thread.Start();

7. Các mẹo hữu ích

Hook toàn cục cho ngoại lệ chưa bắt của luồng

AppDomain.CurrentDomain.UnhandledException += (sender, args) =>
{
    Console.WriteLine($"Bắt toàn cục lỗi: {((Exception)args.ExceptionObject).Message}");
};

Thread thread = new Thread(() =>
{
    throw new Exception("Exterminatus!");
});
thread.Start();

Handler AppDomain.CurrentDomain.UnhandledException được gọi cho ngoại lệ chưa bắt trong luồng, nhưng không cho phép "hồi sinh" luồng hoặc ngăn chặn quá trình kết thúc trong .NET Framework. Trong .NET (Core/5+) nó log lỗi; ứng dụng có thể tiếp tục nếu các luồng khác vẫn hoạt động.

Sự khác biệt trong xử lý ngoại lệ — Thread vs Task

Thread
Task
Nơi bắt Bên trong luồng Ở mã gọi (await, ContinueWith, v.v.)
Hệ quả Ngoại lệ bị mất/kill luồng (hoặc cả app trong .NET Framework) Ngoại lệ đến chỗ chờ (await)
Thông báo lên trên Chỉ rõ ràng (biến, sự kiện, queue) Qua await, AggregateException khi chờ đồng bộ
Logging Cần làm thủ công trong mã luồng Thường ở try-catch quanh await
Context Độc lập với luồng cha Task dùng synchronization context của mã gọi (ví dụ UI-context trong WPF)

8. Lỗi thường gặp khi làm việc với ngoại lệ trong Thread

Lỗi #1: không bắt ngoại lệ trong luồng.
Kết quả là bạn có thể nhận phần ứng dụng bị kết thúc một cách âm thầm, hoặc đôi khi toàn bộ process sập, mà không có chẩn đoán rõ ràng.

Lỗi #2: cố "bắt" ngoại lệ từ luồng trong luồng chính.
Điều này không hiệu quả: try-catch quanh thread.Join() hoặc thread.Start() sẽ không bắt được lỗi bị ném bên trong luồng.

Lỗi #3: mất thông tin lỗi.
Nếu luồng sập mà bạn không truyền ngoại lệ rõ ràng (biến, queue, sự kiện), bạn sẽ không biết nguyên nhân hay chi tiết. Điều này dẫn đến bug "ma".

Lỗi #4: thiếu logging.
Luôn log lỗi trong luồng, ngay cả khi có vẻ "không nghiêm trọng".

Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION