Обработка исключений Parallel LINQ

48

Если в последовательном запросе LINQ что-то идет не так, генерируемое исключение прерывает любую дальнейшую обработку. Например, если при обработке названия машины Aston Martin генерируется исключение, ни одно из имен, следующих за Aston Martin, не будет обработано, как показано на рисунке:

Исключение в последовательном запросе

Таким образом, мы должны получить результаты, предшествующие Aston Martin, но не следующие за ним. Давайте проверим. В примере ниже содержится последовательный запрос, который выбирает названия всех машин и выводит их на консоль. Однако в код добавлен один трюк. При достижении запросом Aston Martin генерируется исключение:

string[] cars = { "Nissan", "Aston Martin", "Chevrolet", "Alfa Romeo", "Chrysler", "Dodge", "BMW",
                              "Ferrari", "Audi", "Bentley", "Ford", "Lexus", "Mercedes", "Toyota", "Volvo", "Subaru", "Жигули :)"};

            // Последовательный запрос
            IEnumerable<string> auto = cars
                .Select(p =>
                {
                    if (p == "Aston Martin")
                        throw new Exception("Проблемы с машиной " + p);
                    return p;
                });

            try
            {
                foreach (string s in auto)
                    Console.WriteLine("Результат: " + s + "\n");
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }

После компиляции и запуска кода получаются показанные ниже результаты, которых можно было ожидать. Элемент Nissan обрабатывается корректно, а затем возникает проблема (вызванная намеренно) с Aston Martin. Сгенерированное исключение прекращает выполнение оставшейся части запроса:

Принудительное исключение в последовательном запросе

Но с PLINQ все обстоит иначе. Вспомните, что данные разбиты на разделы, которые затем обрабатываются независимо и параллельно. Может случиться так, что будет сгенерировано более одного исключения, и поскольку одновременно обрабатывается несколько разделов, то первое исключение не останавливает обработку. На рисунке ниже показано, как это происходит:

Выполнение параллельного запроса с исключением

На приведенном рисунке четыре раздела обрабатываются параллельно. В первом разделе возникает проблема с Aston Martin, которая приводит к генерации исключения. Однако это не останавливает обработку других разделов, а проблема с Ford вызывает генерацию второго исключения. Так что же делать?

К счастью, существует изящное решение. PLINQ собирает все исключения, которые находит, и упаковывает их в System.AggregateException, которое затем генерирует в коде. Ниже содержится запрос PLINQ, который сгенерирует исключения для значений Aston Martin и Ford:

string[] cars = { "Nissan", "Aston Martin", "Chevrolet", "Alfa Romeo", "Chrysler", "Dodge", "BMW",
                              "Ferrari", "Audi", "Bentley", "Ford", "Lexus", "Mercedes", "Toyota", "Volvo", "Subaru", "Жигули :)"};

            // Запрос Parallel LINQ
            IEnumerable<string> auto = cars
                .AsParallel()
                .Select(p =>
                {
                    if (p == "Aston Martin" || p == "Ford")
                        throw new Exception("Проблемы с машиной " + p);
                    return p;
                });

            try
            {
                foreach (string s in auto)
                    Console.WriteLine("Результат: " + s + "\n");
            }
            catch (AggregateException agex)
            {
                agex.Handle(ex =>
                {
                    Console.WriteLine(ex.Message);
                    return true;
                });
            }

Когда начинается перечисление результатов, цикл foreach помещается в блок try/catch, который ищет AggregateException. Класс AggregateException имеет метод Handle, позволяющий обрабатывать каждое исключение по очереди. Ему передается исключение и возвращается true, если оно обработано, либо false, если обработать исключение не удается.

Если исключение не обрабатывается, оно распространяется дальше и в конечном итоге прекращает работу программы. С другой стороны, не имеет смысла обрабатывать исключения, которые не ожидались, и не известно, что с ними делать. Это путь к непредсказуемому поведению и трудно обнаруживаемым ошибкам.

Результаты, которые получаются от запроса PLINQ, когда возникло исключение, непредсказуемы. Все зависит от того, как PLINQ разделяет данные и сколько разделов обрабатывается параллельно.

Пройди тесты
Лучший чат для C# программистов