воскресенье, 26 июня 2011 г.

Класс Parallel


Новым классом для абстрагирования функциональности потоков в .Net 4.0 является класс Parallel. В своей работе этот класс  использует множество задач и потоков. Класс  Parallel содержит несколько статических методов, позволяющих выполнять параллельные циклы for или foreach, а также позволяет организовать параллельный вызов разных методов.
Ниже представлен список основных методов содержащихся в классе Parallel:

Имя
Описание
Выполняет цикл for, в котором итерации могут выполняться параллельно.
Выполняет цикл for, обеспечивая возможность параллельного выполнения итераций, а также контроля состояния цикла и управления этим состоянием.
Выполняет операцию foreach (For Each в Visual Basic) для объекта IEnumerable, обеспечивая возможность параллельного выполнения итераций.
Выполняет операцию foreach (For Each в Visual Basic) для объекта IEnumerable, обеспечивая возможность параллельного выполнения итераций, а также контроля состояния цикла и управления этим состоянием.
Выполняет все предоставленные действия, в том числе параллельно.
Выполняет каждое из указанных действий по возможности в параллельном режиме, если операция не отменена пользователем.
Данные методы имеют ещё очень много перегрузок, мы не будем здесь все их рассматривать. Рассмотрим самые простые For и Foreach.

Организация циклов с помощью Parallel.For

Метод Parallel.For делает то же самое что и обычный метод оператор for, за исключением того что все итерации выполняются параллельно. Порядок выполнения итераций не определён. Первый параметр в методе это - начальный индекс (включительно) цикла, второй параметр – это конечный индекс (не включительно) цикла, третим же параметром передаётся делегат, который вызывается один раз за итерацию. Данный делегат предоставляется с числом итераций (Int32) в виде параметра. Возвращаемым типом у Parallel.For является структура ParallelLoopResult, в которой содержатся сведения о выполненной части цикла.
Тип ParallelLoopResult предоставляет следующие члены.

Имя
Описание
Получает значение, указывающее, дошел ли цикл до завершения, то есть все итерации цикла выполнены и он не получил запроса на преждевременное прерывание работы.
Получает индекс нижней итерации, из которой был вызван метод Break.

Если IsCompleted возвращает значение true, значит, цикл был выполнен до завершения, т. е. были выполнены все итерации цикла. Если IsCompleted возвращает false и LowestBreakIteration возвращает null, вызов Stop использовался для завершения цикла преждевременно. Если IsCompleted возвращает значение false и LowestBreakIteration возвращает целочисленное значение, отличное от null, Break использовался для завершения цикла преждевременно.
Рассмотрим пример цикла  Parallel.For.

           
          ParallelLoopResult result =
                Parallel.For(0, 10, i =>
                    {
                        Console.WriteLine("{0}, task: {1}, thread: {2}", i,
                           Task.CurrentId, Thread.CurrentThread.ManagedThreadId);
                        Thread.Sleep(10);

                    });
            Console.WriteLine(result.IsCompleted);


В теле цикла осуществляется вывод на консоль индекса, идентификатора задачи и идентификатора потока. Вот что мы увидим в консоли.

 № 0, task: 1, thread: 1
№ 1, task: 2, thread: 3
№ 2, task: 3, thread: 4
№ 3, task: 2, thread: 1
№ 4, task: 1, thread: 4
№ 5, task: 2, thread: 3
№ 6, task: 3, thread: 3
№ 7, task: 1, thread: 1
№ 8, task: 2, thread: 4
№ 9, task: 3, thread: 4
True
Для продолжения нажмите любую клавишу . . .

Как видно в приведённом выше выводе точный парядок не гарантируется.
Выполнение метода Parallel.For можно прекращать. В одной из перегрузок этого метода есть третий параметр Action<int, ParallelLoopState>. Использовав эту перегрузку метода, мы можем оказывать влияние на выполнение цикла с помощью методов Break() и Stop() объекта ParallelLoopState. Stop может использоваться для передачи циклу информации о том, что выполнять другие итерации не требуется.
Для продолжительных итераций, которые могут уже выполняться, Stop вызывает возврат присвоив свойству IsStopped значения true для всех остальных итераций цикла, таким образом, что другая итерация может проверить IsStopped и завершить работу раньше, если свойство имеет значение true.
Stop обычно применяется в алгоритмах поиска, где после нахождения результата, выполнять другие итерации не требуется.
Break может использоваться для передачи циклу информации, что другие итерации после текущей итерации выполнять не требуется. Например, если Break вызывается из сотой итерации цикла for, параллельно выполняющегося от 0 до 1000, все итерации меньше 100 должны по-прежнему выполняться, а итерации от 101 до 1000 являются ненужными
Для продолжительных итераций, которые могут уже выполняться, Break вызывает присвоение свойству LowestBreakIteration индекса текущей итерации, если текущий индекс меньше, чем текущее значение LowestBreakIteration. Break обычно применяется в алгоритмах на основе поиска, где присутствует порядок в источнике данных.


            ParallelLoopResult result =
                Parallel.For(10, 40, (int i, ParallelLoopState pls) =>
                    {
                        Console.WriteLine("i: {0} task {1}", i, Task.CurrentId);
                        Thread.Sleep(10);
                        if (i > 15)
                            pls.Break();
                    });
            Console.WriteLine(result.IsCompleted);
            Console.WriteLine("lowest break iteration: {0}", result.LowestBreakIteration);


Ниже приведён вывод этого приложения, который показывает, что итерация прерывается при значении свыше 15, но остальные задачи могут выполняться параллельно.

i: 24 task 2
i: 31 task 3
i: 10 task 1
i: 38 task 5
i: 17 task 4
i: 11 task 1
i: 12 task 1
i: 13 task 1
i: 14 task 1
i: 15 task 1
i: 16 task 1
False
lowest break iteration: 16
Для продолжения нажмите любую клавишу . . .

Для выполнения цикла может использоваться несколько потоков. Если в каждом потоке требуется инициализация, следует принимать метод Parallel.For<TLocal>(). Рассмотрим пример.

          
 Parallel.For<string>(0, 20,
                () =>
                {
                    // invoked once for each thread
                    Console.WriteLine("init thread {0}, task {1}", Thread.CurrentThread.ManagedThreadId, Task.CurrentId);
                    return String.Format("t{0}", Thread.CurrentThread.ManagedThreadId);
                },
                (i, pls, str1) =>
                {
                    // invoked for each member
                    Console.WriteLine("body i {0} str1 {1} thread {2} task {3}", i, str1,
                        Thread.CurrentThread.ManagedThreadId,
                        Task.CurrentId);
                    Thread.Sleep(10);
                    return String.Format("i {0}", i);

                },
                (str1) =>
                {
                    // final action on each thread
                    Console.WriteLine("finally {0}", str1);
                });


Второй делегат вызывается один раз для каждого значения в диапазоне перебора. Предоставляется со следующими параметрами: число итераций (Int32), экземпляр ParallelLoopState, который может использоваться для преждевременного выхода из цикла, и некоторое значение, которое может быть общим для итераций, которые выполняются в одном и том же потоке.
Первый делегат вызывается по одному разу для каждого потока, участвующего в выполнении цикла, и возвращает начальное значение для каждого из этих потоков. Эти начальные состояния передаются второму делегату в каждом потоке. Затем вызов каждого последующего тела возвращает потенциально измененное значение состояния, которое передается следующему вызову тела. Наконец вызов последнего тела в каждом потоке возвращает значение состояния, передаваемое третьему делегату. Третий делегат вызывается один раз для каждого потока, чтобы выполнить окончательное действие каждого потока. Этот делегат может быть вызван одновременно несколькими потоками; поэтому необходимо синхронизировать доступ ко всем общим переменным.
Ниже представлен результат выполнения вышеописанной программы.

init thread 1, task 6
init thread 4, task 7
body i 10 str1 t4 thread 4 task 7
init thread 3, task 10
body i 1 str1 t3 thread 3 task 10
body i 0 str1 t1 thread 1 task 6
init thread 6, task 9
init thread 5, task 8
body i 15 str1 t5 thread 5 task 8
body i 5 str1 t6 thread 6 task 9
body i 11 str1 i 10 thread 4 task 7
body i 2 str1 i 1 thread 3 task 10
body i 16 str1 i 15 thread 5 task 8
body i 6 str1 i 5 thread 6 task 9
body i 4 str1 i 0 thread 1 task 6
body i 12 str1 i 11 thread 4 task 7
body i 3 str1 i 2 thread 3 task 10
body i 17 str1 i 16 thread 5 task 8
body i 7 str1 i 6 thread 6 task 9
body i 8 str1 i 4 thread 1 task 6
body i 13 str1 i 12 thread 4 task 7
body i 18 str1 i 3 thread 3 task 10
finally i 17
body i 9 str1 i 8 thread 1 task 6
finally i 7
body i 14 str1 i 13 thread 4 task 7
body i 19 str1 i 18 thread 3 task 10
finally i 9
finally i 14
finally i 19
Для продолжения нажмите любую клавишу . . .

Организация циклов с помощью Parallel.Foreach

 Выполняет операцию foreach для объекта IEnumerable, обеспечивая возможность параллельного выполнения итераций. Конкретный порядок прохода не определён. Рассмотрим пример.

          
  string[] data = {"zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten", "eleven", "twelve"};

            ParallelLoopResult result =
                Parallel.ForEach<string>(data, s =>
                    {
                        Console.WriteLine(s);
                    });


Вот что мы увидели в консоли.

nine
ten
eleven
seven
eight
four
five
twelve
six
one
two
Для продолжения нажмите любую клавишу . . .

Чтобы цикл можно было прервать, нужно использовать перегрузку этого метода с параметром ParallelLoopStaye.

   
        Parallel.ForEach<string>(data,
                (s, pls, l) =>
                {
                    Console.WriteLine("{0} {1}", s, l);

                });


Вот что мы получили на консоли.

zero 0
one 1
two 2
four 4
five 5
seven 7
eight 8
ten 10
eleven 11
three 3
six 6
twelve 12
nine 9
Для продолжения нажмите любую клавишу . . .
Вызов множества методов с помощью Parallel.Invoke

 Метод Parallel.Invoke позволяет выполнять параллельное выполнение множества методов. Parallel.Invoke  может принимать массив делегатов Action, в которых указываются выполняемые методы. Нет гарантий о порядке, в котором выполнение операции выполняются, или о том, выполняются ли они параллельно. Этот метод не осуществляет возврат до завершения каждой из предоставленного операций, независимо от того, является завершение плановым или в результате исключения.
Ниже приведён пример параллельного выполнения методов Foo и Bar.

        static void ParallelInvoke()
        {
            Parallel.Invoke(Foo, Bar);
        }

        static void Foo()
        {
            Console.WriteLine("foo");
        }

        static void Bar()
        {
            Console.WriteLine("bar");
        }

Ниже видим, то что вывела на консоль программа.

foo
bar
Для продолжения нажмите любую клавишу . . .

Обратите внимание, что с помощью метода Invoke() просто указываются действия, которые необходимо выполнить параллельно, а среда выполнения обрабатывает все сведения по планированию потока, включая автоматическое масштабирование на число ядер в главном компьютере.
В этом примере параллельно обрабатываются операции, а не данные. Как вариант, можно параллельно обрабатывать запросы LINQ с помощью PLINQ и выполнять запросы последовательно. Кроме того, можно параллельно обрабатывать данные с помощью PLINQ. Другим вариантом является параллельная обработка запросов и задач. Хотя полученная нагрузка может снизить производительность главных компьютеров с относительно небольшим количеством процессоров, масштабирование будет выполняться гораздо лучше на компьютерах с множеством процессоров.
Надеюсь, статья была для вас полезной и интересной.

Комментариев нет:

Отправить комментарий