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

Исключения в Parallel.For и Parallel.ForEach

C# SELF
61 уровень , 2 лекция
Открыта

1. Работа исключений в Parallel.For и Parallel.ForEach

В обычном for-цикле всё просто: если внутри тела цикла бросается исключение — выполнение цикла завершается, и исключение летит наружу. В параллельных циклах не так. Давайте разбираться.

Все исключения собираются в один "мешок"

Когда внутри одной из итераций параллельного цикла (Parallel.For/ForEach) возникает исключение, оно не сразу летит наружу, а запаковывается. Процесс продолжается: другие итерации либо дорабатывают, либо также выкидывают исключения. Итог: когда параллельный цикл завершает исполнение (или вынужденно прерывается), все "выброшенные" исключения собираются и выбрасываются наружу в виде одного объекта типа AggregateException.

AggregateException — это "контейнер", который внутри хранит коллекцию всех исключений, произошедших во время выполнения параллельных итераций. Это удобно: мы всегда получаем ВСЕ ошибки (или хотя бы все, что успели накопиться до завершения основных потоков).

Как это выглядит на практике

Пример: параллельная обработка, где иногда бросаем исключение

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 =>
            {
                // Мы намеренно делим на число, иногда оно равно нулю!
                // Это вызовет DivideByZeroException
                int result = 100 / number;
                Console.WriteLine($"100 / {number} = {result}");
            });
        }
        catch (AggregateException ex)
        {
            Console.WriteLine("Обнаружены ошибки в параллельном цикле!");

            // Перебираем все исключения, которые случились
            foreach (var inner in ex.InnerExceptions)
            {
                Console.WriteLine($"Тип: {inner.GetType().Name} — Сообщение: {inner.Message}");
            }
        }
    }
}

Что произойдет:

  • В основной коллекции есть нули, а деление на 0 — это табу в математике (и в C#): возникнут DivideByZeroException.
  • Параллельный цикл начинает обработку. Как только где-то произойдет деление на ноль — цикл не остановится сразу, а продолжит все итерации, которые уже начали выполнение.
  • Когда все потоки завершат работу (кто с ошибкой, кто без), наружу полетит AggregateException, содержащий все случившиеся исключения.

Визуализируем механику обработки исключений

flowchart LR
    A[Поток 1]
    B[Поток 2]
    C[Поток 3]
    D[Поток 4]
    E[Parallel.ForEach]
    F[Исключение 1]
    G[Исключение 2]
    H[AggregateException]
    subgraph Итерации
      A --> F
      B --> G
      C --> E
      D --> E
      F --> H
      G --> H
      E --> H
    end

На схеме видно: разные потоки могут столкнуться с разными ошибками, и все они в итоге "пакуются" в единый AggregateException.

2. Практические особенности обработки ошибок

Что делать с AggregateException?

Когда ловим AggregateException, обычно есть два сценария:

  • Вывести пользователю (или в лог) все ошибки, чтобы богатеть опытом.
  • Понять, какая ошибка критична, а какие — ерунда: решить, считать ли всю операцию неудачной или игнорировать отдельные сбои.

Типичный паттерн: обработка через Handle

try
{
    Parallel.For(0, 10, i =>
    {
        if (i == 3 || i == 7)
            throw new InvalidOperationException($"Ошибка в итерации {i}");
        Console.WriteLine($"Обработано: {i}");
    });
}
catch (AggregateException ex)
{
    ex.Handle(e =>
    {
        if (e is InvalidOperationException)
        {
            Console.WriteLine("Поймана ошибка: " + e.Message);
            // true = ошибка считается обработанной
            return true;
        }
        // false = не обработано, бросит снова
        return false;
    });
}

Такой подход позволяет обработать только те ошибки, которые вы считаете "нормой", а всё остальное — пропускать наверх, чтобы не пропустить критические сбои.

Интересные (и опасные) нюансы реализации

Когда цикл останавливается?
Когда в итерации возникает исключение, Parallel.For/ForEach не запускает новые итерации, но уже начатые продолжают выполняться. После завершения всех активных итераций выбрасывается AggregateException. Если потоков много, "хвост" работы всё равно дойдёт до завершения — поэтому ошибок может быть несколько.

Если не ловить исключение, приложение упадёт.
Если не обернуть Parallel.For/ForEach в блок try-catch, приложение завершится аварийно на первой же встреченной ошибке после завершения всех итераций — не очень вежливо по отношению к пользователю.

Передача исключения "внутрь" цикла.
Иногда нужен особый подход. Например, если вы хотите, чтобы отдельные итерации не портили всю картину, можно обработать исключения прямо в теле параллельного цикла:

Parallel.ForEach(numbers, number =>
{
    try
    {
        int result = 100 / number;
        Console.WriteLine(result);
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Ошибка на числе {number}: {ex.Message}");
    }
});

Такой способ хорош, если вам не нужны все исключения "оптом" — вы сразу обрабатываете каждую неудачу на месте (например, пишете в лог). Но будьте осторожны: если вы так делаете — никакая AggregateException не возникнет, и вы не сможете узнать, было ли всё в порядке в целом.

Если вызывается Break() или Stop().
Если итерация вызывает ParallelLoopState.Break() или ParallelLoopState.Stop(), цикл пытается остановить новые итерации: Break() завершает итерации после текущего индекса, а Stop() — все итерации. Однако, если одновременно возникает исключение, оно сохраняется и выбрасывается в виде AggregateException после завершения всех активных итераций.

3. Полезные нюансы

Исключения в обычных циклах vs параллельных

В обычном цикле любая ошибка приводит к немедленному завершению всей работы: исключение летит наружу, всё блокируется.

В параллельных циклах C# придерживается более компромиссного подхода: работа продолжается для уже стартовавших задач, и только по завершении всего процесса все ошибки "выходят" наружу одной порцией. Это позволяет собрать все ошибки, не потеряв ни одной, и принять решение после завершения цикла.

4. Типичные ошибки при работе с исключениями в Parallel.For и Parallel.ForEach

Ошибка №1: игнорирование AggregateException.
Если не поймать AggregateException, приложение завершится аварийно после завершения всех итераций, что приведёт к потере данных и сбоям в серверных или GUI-приложениях.

Ошибка №2: использование .Wait() без try-catch.
Вызов .Wait() для Parallel.For/ForEach без обработки AggregateException приведёт к необработанному исключению, что усложнит диагностику.

Ошибка №3: игнорирование повторяющихся ошибок.
Множественные одинаковые ошибки (например, деление на ноль) могут быть выброшены из-за повторяющихся данных. Без анализа InnerExceptions можно упустить корень проблемы.

Ошибка №4: глушение всех исключений.
Использование catch (Exception) { /* пусто */ } внутри цикла скрывает ошибки, что приводит к потере важной информации и "призрачным" багам.

Поведение ошибок в разных циклах

Вариант Обычный for/foreach Parallel.For / ForEach
Исключение обрабатывается Немедленно После завершения всех итераций
Формат ошибки Одиночный exception AggregateException с коллекцией
Остальные итерации Не выполняются Уже запущенные дорабатывают
Ловля ошибок в теле Да Да
Ловля ошибок "снаружи" Да Да, через AggregateException

"Фишки" и короткие вопросы для интервью:

  • Что будет, если не обрабатывать AggregateException?
    Приложение упадёт после завершения всех итераций — независимо от того, где и когда произошла ошибка.
  • Можно ли узнать, в какой именно итерации возникла ошибка?
    Только если вы сами включите в исключение информацию об индексе или данных.
  • Может ли AggregateException быть пустым?
    Нет, он создаётся только при наличии хотя бы одного внутреннего исключения. Если ошибок нет, он не выбрасывается.
  • Обрабатываются ли ошибки, если их поймать внутри цикла?
    Да, но тогда "наружу" уже ничего не вылетит, и AggregateException не возникнет.

Теперь вы готовы не только запускать циклы в несколько потоков, но и ловко разруливать все их параллельные "аварии"! И, как всегда, — будьте осторожны с многопоточностью: она любит сюрпризы, особенно если их никто не ловит.

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