не хватает одного параметра: static void Main(string[] args) |
Распараллеливание циклов. Класс Parallel
Управление циклом при распараллеливании
Напомню, метод Parallel.For имеет множество реализаций и по синтаксису является функцией, возвращающей значение типа ParallelLoopResult. До сих пор метод вызывался как оператор, и возвращаемое значение никак не использовалось. Попробуем разобраться, как и в каких ситуациях следует использовать значение, возвращаемое методом.
Рассмотрим обычный оператор цикла for. Возможны следующие ситуации при его выполнении:
- Нормальное завершение. Все итерации цикла завершились без каких-либо происшествий.
- Преждевременное завершение итерации. В ходе итерации выполнен оператор continue, что ведет к прерыванию текущей итерации и переходу на следующую итерацию.
- Преждевременное завершение цикла. В ходе итерации выполнен оператор break, что ведет к прерыванию текущей итерации и выходу из цикла.
- Аварийное завершение итерации. В ходе итерации возникла исключительная ситуация, что ведет к прерыванию цикла и необходимости обработки возникшей ситуации.
Спроецируем эти четыре варианта на работу цикла Parallel.For. Когда работает этот цикл, то одновременно могут выполняться несколько итераций, другие могут ждать своей очереди, порядок запуска итераций произвольный, нельзя сказать, какая итерация будет выполняться первой, а какая - последней.
В случае нормального завершения всех итераций и в параллельном случае никаких особых действий предпринимать не нужно. Возвращаемое значение метода For в этом случае сообщает об успехе выполнения.
Если одна или несколько итераций завершаются преждевременно, то и здесь кроме завершения текущей итерации ничего предпринимать не нужно, поскольку другие итерации запускаются автоматически.
Если на итерации с номером i выполнился метод break, то цикл необходимо завершить. Для параллельного выполнения это означает завершение всех уже запущенных итераций, запуск на выполнение всех еще не запущенных итераций с номерами, меньшими i. Запуск итераций с номерами, большими i, отменяется, если они конечно еще не были запущены. Заметьте, поскольку одновременно выполняются несколько итераций, то break может выполняться не один раз. Если например break выполнился на итерациях с номерами 10 и 20, то запускать нужно итерации, номера которых меньше 10.
Если на итерации с номером i возникла исключительная ситуация, то цикл необходимо завершить, а ситуацию обработать. Поскольку одновременно выполняются несколько итераций, то исключительные ситуации могут возникать на разных итерациях. Поэтому выбрасывается агрегирующая исключительная ситуация Aggregation Exception, которая содержит информацию обо всех возникших исключительных ситуациях. Следует предусмотреть catch блок, обрабатывающий эту ситуацию.
Как же выполнить оператор break при параллельном исполнении? Для этого нужно использовать перегруженную версию Parallel.For, в которой методу, выполняющему итерацию, помимо индекса цикла передается дополнительный параметр класса ParallelLoopState. Объект этого класса может вызывать метод Break, который и реализует стратегию прерывания для параллельного выполнения. Анализируя возвращаемое значение метода Parallel.For, можно узнать минимальный номер итерации, на которой впервые выполнялось прерывание (break). Помимо метода Break объект класса ParallelLoopState может вызывать и метод Stop, который также завершает все выполняемые итерации, но, в отличие от Break, не вызывает на исполнение итерации с меньшими номерами. Вызов Stop означает остановить выполнение. В этом случае теряет смысл понятие итерации с минимальным номером, прервавшей выполнение.
Давайте рассмотрим пример на тему прерывания исполнения. В программировании есть знаменитая проблема, связанная с целыми числами, которую иногда называют проблемой "3x + 1", а сами преобразуемые числа называют числами - градинами. Суть ее в следующем: если взять любое целое число и проводить над ним в цикле достаточно простые преобразования, получая новое число, то результат обязательно сойдется к единице. Строго обосновать этот факт, доказав завершаемость достаточно простого цикла, пока никому не удалось.
Можно провести серию экспериментов с различными числами и посмотреть на процесс их преобразования. Некоторые числа сходятся к единице быстро, некоторые - медленно. Нас будет интересовать вопрос, для каких чисел процесс преобразования затягивается. Напишем программу, которая будет обнаруживать числа, для которых число преобразований превосходит заданный предел. Преобразования над разными числами будем выполнять параллельно:
/// <summary> /// Проблема 3x + 1 /// Поиск медленно сходящихся чисел-градин /// </summary> static void Break_Stop_Test() { ParallelLoopResult res; res = Parallel.For(2, n, Grad); Console.WriteLine(res.LowestBreakIteration.ToString()); Console.WriteLine(res.IsCompleted.ToString()); }
Значение, возвращаемое методом Parallel.For, представляет структуру ParallelLoopResult, у которой два поля. Булевское поле IsCompleted возвращает значение true, если все итерации закончились без происшествий, и false, в противном случае. Если на одной или нескольких итерациях выполнялся break, то поле LowestBreakIteration вернет значение минимальной итерации.
Приведу текст метода Grad, выполняющего итерацию:
static void Grad(int i, ParallelLoopState pls) { int N = i; const int m = 150; int k =0; while (N != 1 ) { if (N % 2 == 0) N = N / 2; else N = 3 * N + 1; k++; // if (k == m) pls.Stop(); if (k == m) pls.Break(); } }
Методу передаются два параметра. Объект pls класса ParallelLoopState позволяет в нужный момент вызвать метод Break или метод Stop. В данном исследовании нас интересует именно Break, чтобы обнаружить самое маленькое число, на котором впервые достигается заданный предел. Поэтому возможность вызова метода Stop показана, но закомментирована.
У объектов класса ParallelLoopState есть ряд полезных свойств, позволяющих разным итерациям цикла обмениваться информацией о возникающих событиях. Но мы не будем демонстрировать эти возможности.
Наш пример позволяет обнаружить первое число, которое после проведения m шагов цикла while все еще не сошлось к единице. Вот результаты выполнения нашего теста:
Число 703 - это первое число, для которого требуется не менее 150 преобразований, чтобы оно сошлось к единице. Цикл был прерван, не все его итерации были выполнены, на что указывает значение false свойства IsCompleted.
Управление исключительными ситуациями при распараллеливании
Рассмотрим теперь пример, отражающий организацию обработки исключительных ситуаций, которые могут возникать в ходе выполнения параллельного цикла. Будем моделировать работу прибора, управляющего температурным режимом. Предполагается, что числа, появляющиеся на итерации, задают значение температуры. На итерации могут возникать исключения двух моделируемых нами типов. Если полученное значение температуры слишком велико, то "выбрасывается" исключение, свидетельствующее о "высокой температуре". В другой ситуации "выбрасывается" исключение, свидетельствующее о "низкой температуре".
Вот как устроен наш тест:
/// <summary> /// Выбрасывание исключений и их обработка /// </summary> static void HLTest() { ParallelLoopResult res = new ParallelLoopResult(); try { res = Parallel.For(0, n, Temperature); } catch (LowTemperatureException e) { Console.WriteLine(e.Message); } catch (HighTemperatureException e) { Console.WriteLine(e.Message); } catch (AggregateException ae) { Console.WriteLine(ae.Message); ae.Handle((x) => { if (x is HighTemperatureException) { Console.WriteLine( "Агрегированное сообщение: Высокая температура"); return true; } else if (x is LowTemperatureException) { Console.WriteLine( "Агрегированное сообщение: Низкая температура"); return true; } else return false; }); } finally { Console.WriteLine(res.IsCompleted.ToString()); Console.WriteLine(res.LowestBreakIteration.ToString()); } }
Метод Parallel.For помещается в try-блок. После try-блока следуют три catch-обработчика исключительной ситуации. Заметьте, первые два бесполезны, несмотря на то, что они пытаются перехватить фактически возникающие исключения. Дело в том, что при параллельном выполнении все исключения перехватываются и собираются в одно агрегированное исключение AggregateException, которое и перехватывает специальный catch-обработчик.
У объектов типа AggregateException есть замечательный метод Handle, позволяющий получить все исключения, возникшие на параллельно исполняемых итерациях цикла. Эти исключения можно тут же проанализировать и выполнить их обработку. Наш пример демонстрирует этот способ обработки исключений.
Давайте рассмотрим метод, выполняющий итерацию, в ходе которой и могут появляться исключения того или иного типа:
static void Temperature(int i, ParallelLoopState pls) { const int m = 1000; //моделируем показания прибора int N = rnd.Next(-m, m); if (N > 777) throw new HighTemperatureException( "Высокая температура"); if (N < -777) throw new LowTemperatureException( "Низкая температура"); }
Пример прост и в комментариях не нуждается. Для полноты картины приведем классы, описывающие моделируемые нами исключения:
class HighTemperatureException : Exception { public HighTemperatureException() { } public HighTemperatureException(string message): base(message){ } public HighTemperatureException(string message, Exception e) : base(message, e) { } } class LowTemperatureException : Exception { public LowTemperatureException() { } public LowTemperatureException(string message) : base(message){ } public LowTemperatureException(string message, Exception e) : base(message, e) { } }
В завершение приведу результаты одного сеанса выполнения теста: