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

Ngoại lệ trong Parallel.ForParallel.ForEach

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

1. Cách hoạt động của ngoại lệ trong Parallel.ForParallel.ForEach

Trong vòng for thông thường thì đơn giản: nếu trong thân vòng có ngoại lệ — vòng dừng ngay, và ngoại lệ bay ra ngoài. Ở vòng song song thì không như vậy. Cùng tìm hiểu.

Tất cả ngoại lệ được gom vào một "túi"

Khi trong một trong các lần lặp của vòng song song (Parallel.For/ForEach) xảy ra ngoại lệ, nó không bay ra ngoài ngay mà được đóng gói. Quá trình tiếp tục: các lần lặp khác hoặc hoàn thành, hoặc cũng ném ngoại lệ. Kết quả: khi vòng song song kết thúc (hoặc bị buộc dừng), tất cả ngoại lệ "bị ném" được gom lại và ném ra ngoài dưới dạng một đối tượng kiểu AggregateException.

AggregateException là một "container" chứa tập hợp tất cả ngoại lệ xảy ra trong các lần lặp song song. Tiện lợi: ta luôn có TẤT CẢ lỗi (hoặc chí ít là những lỗi đã kịp tích lũy trước khi các luồng chính kết thúc).

Trông nó như thế nào trong thực tế

Ví dụ: xử lý song song, đôi khi ném ngoại lệ

using System;
using System.Threading.Tasks;

class Program
{
    static void Main()
    {
        int[] numbers = { 1, 2, 0, 4, 0, 6, 7, 8 };

        try
        {
            Parallel.ForEach(numbers, number =>
            {
                // Chúng ta cố ý chia cho một số, đôi khi nó bằng 0!
                // Điều này sẽ gây ra DivideByZeroException
                int result = 100 / number;
                Console.WriteLine($"100 / {number} = {result}");
            });
        }
        catch (AggregateException ex)
        {
            Console.WriteLine("Đã phát hiện lỗi trong vòng lặp song song!");

            // Duyệt qua tất cả ngoại lệ đã xảy ra
            foreach (var inner in ex.InnerExceptions)
            {
                Console.WriteLine($"Loại: {inner.GetType().Name} — Thông báo: {inner.Message}");
            }
        }
    }
}

Sẽ xảy ra:

  • Trong mảng có số 0, và chia cho 0 là điều cấm trong toán học (và trong C#): sẽ xuất hiện DivideByZeroException.
  • Vòng song song bắt đầu xử lý. Ngay khi ở đâu đó xảy ra chia cho 0 — vòng không dừng ngay, mà các lần lặp đã bắt đầu vẫn tiếp tục chạy.
  • Khi tất cả luồng hoàn thành (ai có lỗi, ai không), ra ngoài sẽ ném AggregateException chứa tất cả ngoại lệ đã xảy ra.

Minh họa cơ chế xử lý ngoại lệ

flowchart LR
    A[Luồng 1]
    B[Luồng 2]
    C[Luồng 3]
    D[Luồng 4]
    E[Parallel.ForEach]
    F[Ngoại lệ 1]
    G[Ngoại lệ 2]
    H[AggregateException]
    subgraph Các lần lặp
      A --> F
      B --> G
      C --> E
      D --> E
      F --> H
      G --> H
      E --> H
    end

Trong sơ đồ thấy rõ: các luồng khác nhau có thể gặp các lỗi khác nhau, và cuối cùng tất cả được "gói" vào một AggregateException.

2. Các lưu ý thực tiễn khi xử lý lỗi

Phải làm gì với AggregateException?

Khi bắt AggregateException, thường có hai kịch bản:

  • Hiện tất cả lỗi cho người dùng (hoặc log) để học hỏi.
  • Xác định lỗi nào là quan trọng, lỗi nào là vặt: quyết định xem toàn bộ thao tác có tính là thất bại hay bỏ qua một vài lỗi riêng lẻ.

Mẫu phổ biến: xử lý qua Handle

try
{
    Parallel.For(0, 10, i =>
    {
        if (i == 3 || i == 7)
            throw new InvalidOperationException($"Lỗi ở vòng lặp {i}");
        Console.WriteLine($"Đã xử lý: {i}");
    });
}
catch (AggregateException ex)
{
    ex.Handle(e =>
    {
        if (e is InvalidOperationException)
        {
            Console.WriteLine("Bắt được lỗi: " + e.Message);
            // true = lỗi được coi là đã xử lý
            return true;
        }
        // false = chưa xử lý, sẽ bị ném lại
        return false;
    });
}

Cách này cho phép xử lý chỉ những lỗi bạn coi là "bình thường", còn phần còn lại — để tuôn ra trên cùng, nhằm không bỏ qua lỗi nghiêm trọng.

Những nuance thú vị (và nguy hiểm) trong triển khai

Khi nào vòng dừng?
Khi một lần lặp ném ngoại lệ, Parallel.For/ForEach sẽ không khởi chạy các lần lặp mới, nhưng những lần đã bắt đầu vẫn tiếp tục thực hiện. Sau khi tất cả các lần lặp đang hoạt động hoàn thành sẽ ném AggregateException. Nếu có nhiều luồng, phần "đuôi" công việc vẫn chạy tới kết thúc — nên có thể có nhiều lỗi.

Nếu không bắt ngoại lệ, ứng dụng sẽ sập.
Nếu không bọc Parallel.For/ForEach trong block try-catch, ứng dụng sẽ kết thúc bất ngờ khi gặp lỗi đầu tiên sau khi các lần lặp hoàn thành — không thân thiện với người dùng.

Đưa việc xử lý ngoại lệ "vào trong" vòng lặp.
Đôi khi cần cách khác. Ví dụ, nếu bạn muốn các lần lặp riêng lẻ không làm hỏng cả bức tranh tổng thể, có thể bắt ngoại lệ ngay trong thân vòng:

Parallel.ForEach(numbers, number =>
{
    try
    {
        int result = 100 / number;
        Console.WriteLine(result);
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Lỗi tại số {number}: {ex.Message}");
    }
});

Cách này tốt nếu bạn không cần gom tất cả ngoại lệ "một lần" — bạn xử lý từng thất bại tại chỗ (ví dụ ghi log). Nhưng cẩn thận: nếu làm vậy — sẽ không có AggregateException, và bạn không thể dễ dàng biết toàn bộ tiến trình có ổn hay không.

Nếu gọi Break() hoặc Stop().
Nếu một lần lặp gọi ParallelLoopState.Break() hoặc ParallelLoopState.Stop(), vòng cố gắng ngăn các lần lặp mới: Break() kết thúc các lần lặp sau chỉ số hiện tại, còn Stop() — tất cả các lần lặp. Tuy nhiên, nếu đồng thời có ngoại lệ, ngoại lệ đó vẫn được lưu và sẽ bị ném ra dưới dạng AggregateException sau khi các lần lặp đang chạy kết thúc.

3. Những lưu ý hữu ích

Ngoại lệ ở vòng thường vs vòng song song

Trong vòng thường bất kỳ lỗi nào cũng dẫn tới kết thúc ngay: ngoại lệ bay ra ngoài, mọi thứ dừng lại.

Trong vòng song song C# chọn cách thỏa hiệp: các tác vụ đã khởi chạy tiếp tục làm việc, và chỉ khi toàn bộ quá trình xong thì tất cả lỗi mới "ra" ngoài cùng một lúc. Điều này cho phép gom hết lỗi mà không bỏ sót, rồi quyết định sau khi vòng kết thúc.

4. Lỗi phổ biến khi làm việc với ngoại lệ trong Parallel.ForParallel.ForEach

Lỗi #1: bỏ qua AggregateException.
Nếu không bắt AggregateException, ứng dụng sẽ sập sau khi tất cả lần lặp hoàn thành, dẫn tới mất dữ liệu và lỗi trên server hoặc GUI.

Lỗi #2: dùng .Wait() mà không có try-catch.
Gọi .Wait() cho Parallel.For/ForEach mà không xử lý AggregateException sẽ dẫn tới ngoại lệ không được xử lý, khiến việc chẩn đoán khó khăn.

Lỗi #3: bỏ qua các lỗi lặp lại.
Nhiều lỗi giống nhau (ví dụ chia cho 0) có thể bị ném nhiều lần do dữ liệu lặp lại. Nếu không phân tích InnerExceptions, bạn có thể bỏ sót nguyên nhân gốc.

Lỗi #4: dập tắt mọi ngoại lệ.
Dùng catch (Exception) { /* trống */ } trong vòng lặp che giấu lỗi, gây mất thông tin quan trọng và bug "ma".

Hành vi lỗi ở các vòng khác nhau

Tình huống for/foreach thường Parallel.For / ForEach
Khi ngoại lệ xuất hiện Ngay lập tức Sau khi tất cả lần lặp kết thúc
Định dạng lỗi Exception đơn lẻ AggregateException chứa tập hợp
Các lần lặp còn lại Không được thực hiện Các lần lặp đã bắt đầu hoàn thành
Bắt lỗi trong thân vòng
Bắt lỗi "từ bên ngoài" Có, qua AggregateException

"Mẹo" và câu hỏi phỏng vấn ngắn:

  • Chuyện gì xảy ra nếu không xử lý AggregateException?
    Ứng dụng sẽ sập sau khi tất cả lần lặp hoàn thành — bất kể lỗi xảy ra ở đâu, khi nào.
  • Có biết được lỗi xảy ra ở lần lặp nào không?
    Chỉ khi bạn tự ghi thông tin về chỉ số hoặc dữ liệu vào trong ngoại lệ.
  • AggregateException có thể rỗng không?
    Không, nó chỉ được tạo khi có ít nhất một inner exception. Nếu không có lỗi, nó sẽ không bị ném.
  • Các lỗi có được xử lý nếu bắt trong thân vòng không?
    Có, nhưng khi đó từ "bên ngoài" sẽ không có gì, và AggregateException sẽ không xuất hiện.

Bây giờ bạn đã sẵn sàng không chỉ chạy vòng lặp đa luồng mà còn xử lý khéo các "tai nạn" song song của chúng! Và, như mọi khi — cẩn thận với đa luồng: nó thích những bất ngờ, nhất là khi không ai bắt chúng.

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