Урок 6. Исключения и параллельные циклы C#
Шестой урок учебника по параллельному программированию в .NET - это последний, в которой рассматриваются параллельные циклы. В нем мы поговорим о том, как исключения генерируются кодом в параллельном цикле и как их можно обрабатывать.
Необработанное исключение
Когда исключение выбрасывается из последовательного цикла, нормальный поток программы прерывается. Управление переходит к следующему доступному блоку catch или, если соответствующие операторы try/catch отсутствуют, необработанное исключение передается в блок catch .NET runtime и программа прерывается. Когда блок try/catch присутствует, но его нет в цикле, дальнейшие итерации не выполняются, и текущая итерация завершается досрочно.
При работе с параллельными циклами For или ForEach обработка исключений несколько усложняется. Когда исключение создается в одном потоке выполнения, вполне вероятно, что существуют другие итерации цикла, выполняющиеся параллельно. Они не могут быть просто прекращены, поскольку это может привести к несоответствиям. Чтобы предотвратить такие ошибки данных, итерации цикла, которые уже были запланированы в других потоках, будут продолжены. Это похоже на вызов ParallelLoopState.Метод Stop для завершения параллельного цикла.
Что параллельные итерации продолжаются после того, как исключение создает вторую проблему. А именно, что произойдет, если другие итерации также выдадут исключения? Ясно, что мы не хотели бы иметь ни одного исключения, вызванного циклом, когда было две или более проблем. Важно захватить каждое исключение, чтобы изящно восстановиться. Чтобы все исключения были собраны вместе, то .NET framework предоставляет класс AggregateException.
AggregateException
Класс AggregateException является подклассом исключения, поэтому предоставляет все стандартные функции исключения. Кроме того, он имеет свойство, которое содержит коллекцию внутренних исключений. При создании параллельного цикла все встречающиеся исключения включаются в свойство, гарантируя, что никакие детали исключения не будут потеряны. Важно отметить, что исключение AggregateException будет вызвано даже в том случае, если во время обработки цикла возникнет только одно исключение. Другие исключения могут быть созданы только в том случае, если существует проблема с самой командой цикла, например делегат действия, определяющий тело цикла, является нулевым.
На следующих уроках мы продемонстрируем обработку исключений для параллельных циклов. Чтобы обеспечить упрощенный доступ к методам цикла добавьте следующую директиву using:
using System.Threading.Tasks;Ловля AggregateExceptions
Для первой демонстрации мы создадим параллельный цикл, который создает исключение. Поскольку мы знаем, что исключение будет обернуто в AggregateException, мы будем ловить только AggregrateExceptions. Цикл For ниже повторяет значения между -10 и 9. каждое значение используется в качестве делителя в простой арифметической операции. Когда значение равно нулю, происходит ошибка деления на ноль и генерируется исключение.
Примечание: при запуске примера кода Visual Studio может прерваться на исключении деления на ноль, чтобы разрешить отладку. Если это так, Нажмите клавишу F5, чтобы продолжить выполнение. Вы можете избежать этой проблемы, нажав Ctrl-F5, чтобы запустить программу без отладки.
try { Parallel.For(-10, 10, i => { Console.WriteLine("100/{0}={1}", i, 100 / i); }); } catch (AggregateException ex) { Console.WriteLine(ex.Message); } /* OUTPUT 100/-10=-10 100/-5=-20 100/-9=-11 100/-6=-16 100/-2=-50 100/-1=-100 100/1=100 100/2=50 100/3=33 100/4=25 100/6=16 100/7=14 100/8=12 100/9=11 100/5=20 100/-8=-12 100/-7=-14 100/-4=-25 100/-3=-33 One or more errors occurred. */Результаты, показанные в приведенных выше комментариях, были получены на четырехъядерном процессоре. Вы можете видеть, что в какой-то момент процесса было обнаружено исключение деления на ноль, и после остановки цикла было поймано исключение AggregateException и выведено его свойство Message. Сообщение простое, указывающее, что AggregateException содержит одно или несколько внутренних исключений. Примечание: если бы мы попытались поймать исключение DivideByZeroException, то исключение было бы необработанным.
Невозможно посмотреть на приведенные выше результаты и понять, когда именно произошло исключение. В последовательном цикле мы могли бы предположить, что это было во время последней обработанной итерации, но в параллельном цикле это может быть неверно. Чтобы понять, что происходит на самом деле, мы можем добавить дополнительную строку в код, который сообщает нам, когда мы собираемся разделить на ноль.
Приведенный ниже пример модифицирован, чтобы показать сообщение непосредственно перед исключением:
try { Parallel.For(-10, 10, i => { if (i == 0) Console.WriteLine("About to divide by zero."); Console.WriteLine("100/{0}={1}", i, 100 / i); }); } catch (AggregateException ex) { Console.WriteLine(ex.Message); } /* ВЫХОД 100/-10=-10 100/5=20 Вот-вот разделится на ноль. 100/-5=-20 100/-4=-25 100/-3=-33 100/-2=-50 100/-1=-100 100/1=100 100/2=50 100/3=33 100/4=25 100/8=12 100/9=11 100/-6=-16 100/-9=-11 100/-8=-12 100/-7=-14 100/6=16 100/7=14 One or more errors occurred. */С новым сообщением мы можем видеть, что исключение произошло очень рано в процессе, после того, как были показаны только два результата расчета. Остальные вычисления выполнялись после первоначального исключения в ранее запланированных итерациях цикла. Вы всегда должны учитывать эту возможность при написании кода параллельного цикла.
Доступ к деталям исключения
После того как вы поймали исключение AggregateException, вы можете изучить каждое из содержащихся в нем исключений, прочитав свойство InnerExceptions. Это возвращает коллекцию только для чтения, которая может быть адресована индексом или перечислена.
Приведенный ниже пример кода демонстрирует использование свойства InnerExceptions путем внесения двух изменений. Во-первых, разделение было изменено, чтобы обеспечить возможность того, что будут выброшены два исключения, а не одно. Они возникают, когда значение управления контуром равно -10 или нулю. Второе изменение заключается в том, что блок catch теперь циклически проходит через свойство InnerExceptions AggregateException и выводит все сообщения об ошибках.
try { Parallel.For(-10, 10, i => { Console.WriteLine("100/{0}={1}", i, 100 / (i % 10)); }); } catch (AggregateException ex) { foreach (Exception inner in ex.InnerExceptions) { Console.WriteLine(inner.Message); } } /* OUTPUT 100/-5=-20 100/5=20 100/6=16 100/7=14 100/8=12 100/9=11 100/-8=-12 100/-7=-14 100/-6=-16 100/-2=-50 100/-1=-100 100/1=100 100/-9=-11 100/2=50 100/3=33 100/4=25 100/-4=-25 100/-3=-33 Attempted to divide by zero. Attempted to divide by zero. */Вызов AggregateExceptions
Иногда вам захочется выбросить свои собственные AggregateExceptions. Например, вы можете выполнить каждую итерацию параллельного цикла, даже если некоторые из этих итераций вызывают исключения. Чтобы исключения не были потеряны, вы можете хранить их в коллекции во время цикла и создавать исключение AggregateException по завершении цикла, инициализируя свойство InnerExceptions списком захваченных исключений. Мы сделаем это в последнем примере в этой статье.
При создании коллекции мы должны использовать потокобезопасный класс, чтобы гарантировать, что никакие исключения не будут потеряны из-за проблем синхронизации. Платформа .NET framework предоставляет несколько таких коллекций. Один из них - ConcurrentQueue
using System.Collections.Concurrent;Для последнего примера я переместил параллельный цикл в свой собственный метод. Это облегчает просмотр того, как добавляется обработка исключений. Перед запуском цикла мы создаем экземпляр нового ConcurrentQueue
После завершения цикла свойство Count очереди проверяется на наличие каких-либо исключений. Если нет, то отображается сообщение о том, что программа завершена без ошибок. При наличии исключений создается новое исключение AggregationException, передающее коллекцию исключений своему конструктору. В основном методе AggregateException перехватывается и выводится значение свойства сообщения каждого исключения.
static void Main() { try { ParallelLoop(); } catch (AggregateException ex) { foreach (Exception inner in ex.InnerExceptions) { Console.WriteLine(inner.Message); } } } static void ParallelLoop() { var exceptions = new ConcurrentQueue(); Parallel.For(-10, 10, i => { try { Console.WriteLine("100/{0}={1}", i, 100 / (i % 10)); } catch (Exception ex) { exceptions.Enqueue(ex); } }); if (exceptions.Count == 0) Console.WriteLine("No exceptions"); else throw new AggregateException(exceptions); } /* OUTPUT 100/-5=-20 100/5=20 100/6=16 100/7=14 100/8=12 100/9=11 100/-8=-12 100/-9=-11 100/-4=-25 100/-3=-33 100/-7=-14 100/-6=-16 100/1=100 100/2=50 100/3=33 100/4=25 100/-2=-50 100/-1=-100 Attempted to divide by zero. Attempted to divide by zero. */
Автор этого материала - я - Пахолков Юрий. Я оказываю услуги по написанию программ на языках Java, C++, C# (а также консультирую по ним) и созданию сайтов. Работаю с сайтами на CMS OpenCart, WordPress, ModX и самописными. Кроме этого, работаю напрямую с JavaScript, PHP, CSS, HTML - то есть могу доработать ваш сайт или помочь с веб-программированием. Пишите сюда.
Отправляя сообщение я подтверждаю, что ознакомлен и согласен с политикой конфиденциальности данного сайта.