Методы For() и ForEach()

24

Метод For()

В TPL параллелизм данных поддерживается, в частности, с помощью метода For(), определенного в классе Parallel. Этот метод существует в нескольких формах. Его рассмотрение мы начнем с самой простой формы, приведенной ниже:

public static ParallelLoopResult
    For(int fromInclusive, int toExclusive, Action<int> body)

где fromInclusive обозначает начальное значение того, что соответствует переменной управления циклом; оно называется также итерационным, или индексным, значением; a toExclusive — значение, на единицу больше конечного. На каждом шаге цикла переменная управления циклом увеличивается на единицу. Следовательно, цикл постепенно продвигается от начального значения fromInclusive к конечному значению toExclusive минус единица.

Циклически выполняемый код указывается методом, передаваемым через параметр body. Этот метод должен быть совместим с делегатом Action<int>.

Для метода For() обобщенный параметр T должен быть, конечно, типа int. Значение, передаваемое через параметр obj, будет следующим значением переменной управления циклом. А метод, передаваемый через параметр body, может быть именованным или анонимным. Метод For() возвращает экземпляр объекта типа ParallelLoopResult, описывающий состояние завершения цикла. Для простых циклов этим значением можно пренебречь.

Главная особенность метода For() состоит в том, что он позволяет, когда такая возможность имеется, распараллелить исполнение кода в цикле. А это, в свою очередь, может привести к повышению производительности. Например, процесс преобразования массива в цикле может быть разделен на части таким образом, чтобы разные части массива преобразовывались одновременно. Следует, однако, иметь в виду, что повышение производительности не гарантируется из-за отличий в количестве доступных процессоров в разных средах выполнения, а также из-за того, что распараллеливание мелких циклов может составить издержки, которые превышают сэкономленное время.

В приведенном ниже примере программы демонстрируется применение метода For() на практике. В начале этой программы создается массив data, состоящий из 1.000.000.000 целых значений. Затем вызывается метод For(), которому в качестве "тела" цикла передается метод MyTransform(). Этот метод состоит из ряда операторов, выполняющих произвольные преобразования в массиве data. Его назначение — сымитировать конкретную операцию. Как будет подробнее пояснено несколько ниже, выполняемая операция должна быть нетривиальной, чтобы параллелизм данных принес какой-то положительный эффект. В противном случае последовательное выполнение цикла может завершиться быстрее:

using System;
using System.Threading.Tasks;

namespace ConsoleApplication12_Parallel_For__
{
    class Program
    {
        static int[] data;

        static void MyTransform(int i)
        {
            data[i] = data[i] / 10;

            if (data[i] < 10000) data[i] = 0;
            if (data[i] >= 10000) data[i] = 100;
            if (data[i] > 20000) data[i] = 200;
            if (data[i] > 30000) data[i] = 300;
        }

        static void Main()
        {
            Console.WriteLine("Основной поток запущен");

            data = new int[100000000];

            for (int i = 0; i < data.Length; i++)
                data[i] = i;

            // Распараллелить цикл методом For()
            Parallel.For(0, data.Length, MyTransform);

            Console.WriteLine("Основной поток завершен");
            Console.ReadLine();
        }
    }
}

Эта программа состоит из двух циклов. В первом, стандартном, цикле for инициализируется массив data. А во втором цикле, выполняемом параллельно методом For(), над каждым элементом массива data производится преобразование. Как упоминалось выше, это преобразование носит произвольный характер и выбрано лишь для целей демонстрации. Метод For() автоматически разбивает вызовы метода MyTransform() на части для параллельной обработки отдельных порций данных, хранящихся в массиве. Следовательно, если запустить данную программу на компьютере с двумя доступными процессорами или больше, то цикл преобразования данных в массиве может быть выполнен методом For() параллельно.

Следует, однако, иметь в виду, что далеко не все циклы могут выполняться эффективно, когда они распараллеливаются. Как правило, мелкие циклы, а также циклы, состоящие из очень простых операций, выполняются быстрее последовательным способом, чем параллельным. Именно поэтому цикл for инициализации массива данных не распараллеливается методом For() в рассматриваемой здесь программе.

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

using System;
using System.Threading.Tasks;
using System.Diagnostics;

namespace ConsoleApplication12_Parallel_For__
{
    class Program
    {
        static int[] data;

        static void MyTransform(int i)
        {
            data[i] = data[i] / 10;

            if (data[i] < 10000) data[i] = 0;
            if (data[i] >= 10000) data[i] = 100;
            if (data[i] > 20000) data[i] = 200;
            if (data[i] > 30000) data[i] = 300;
        }

        static void Main()
        {
            Console.WriteLine("Основной поток запущен");

            // Время выполнения цикла
            Stopwatch sw = new Stopwatch();

            data = new int[100000000];

            sw.Start();

            // Параллельный вариант инициализации массива в цикле
            Parallel.For(0, data.Length, (i) => data[i] = i);

            sw.Stop();
            Console.WriteLine("|| исполнение цикла: {0} секунд",sw.Elapsed.TotalSeconds);
            sw.Reset();

            sw.Start();
            for (int i = 0; i < data.Length; i++)
                data[i] = i;
            sw.Stop();
            Console.WriteLine("Последовательное исполнение цикла: {0} секунд", sw.Elapsed.TotalSeconds);
            sw.Reset();
            Console.WriteLine();

            sw.Start();
            // Распараллелить цикл методом For()
            Parallel.For(0, data.Length, MyTransform);
            sw.Stop();
            Console.WriteLine("|| преобразование данных в цикле: {0} секунд",
                sw.Elapsed.TotalSeconds);
            sw.Reset();

            sw.Start();
            for (int i = 0; i < data.Length; i++)
                MyTransform(i);
            sw.Stop();
            Console.WriteLine("Последовательное преобразование данных в цикле: {0} секунд",
                sw.Elapsed.TotalSeconds);

            Console.WriteLine("Основной поток завершен");
            Console.ReadLine();
        }
    }
}
Преимущества параллельного распределения задач

Прежде всего, обратите внимание на то, что параллельный вариант цикла инициализации массива данных выполняется приблизительно в два раза медленнее, чем последовательный. Дело в том, что в данном случае на операцию присваивания расходуется так мало времени, что издержки на дополнительно организуемое распараллеливание превышают экономию, которую оно дает.

Обратите далее внимание на то, что параллельный вариант цикла преобразования данных выполняется быстрее, чем последовательный. В данном случае экономия от распараллеливания с лихвой возмещает издержки на его дополнительную организацию.

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

Что касается приведенной выше программы, то необходимо упомянуть о двух других ее особенностях. Во-первых, обратите внимание на то, что в параллельно выполняемом цикле для инициализации данных применяется лямбда-выражение. Здесь "тело" цикла указывается в лямбда-выражении. (Напомним, что в лямбдавыражении создается анонимный метод.) Следовательно, для параллельного выполнения методом For() совсем не обязательно указывать именованный метод.

И во-вторых, обратите внимание на применение класса Stopwatch для вычисления времени выполнения цикла. Этот класс находится в пространстве имен System.Diagnostics. Для того чтобы воспользоваться им, достаточно создать экземпляр его объекта, а затем вызвать метод Start(), начинающий отчет времени, и далее — метод Stop(), завершающий отсчет времени. А с помощью метода Reset() отсчет времени сбрасывается в исходное состояние.

Продолжительность выполнения можно получить различными способами. В рассматриваемой здесь программе для этой цели использовано свойство Elapsed, возвращающее объект типа TimeSpan. С помощью этого объекта и свойства TotalSeconds время отображается в секундах, включая и доли секунды. Как показывает пример рассматриваемой здесь программы, класс Stopwatch оказывается весьма полезным при разработке параллельно исполняемого кода.

Как упоминалось выше, метод For() возвращает экземпляр объекта типа ParallelLoopResult. Это структура, в которой определяются два следующих свойства:

public bool IsCompleted { get; }
public Nullable<long> LowestBreakIteration { get; }

Свойство IsCompleted будет иметь логическое значение true, если выполнены все шаги цикла. Иными словами, при нормальном завершении цикла это свойство будет содержать логическое значение true. Если же выполнение цикла прервется раньше времени, то данное свойство будет содержать логическое значение false. Свойство LowestBreakIteration будет содержать наименьшее значение переменной управления циклом, если цикл прервется раньше времени вызовом метода ParallelLoopState.Break().

Для доступа к объекту типа ParallelLoopState следует использовать форму метода For(), делегат которого принимает в качестве второго параметра текущее состояние цикла. Ниже эта форма метода For() приведена в простейшем виде:

public static ParallelLoopResult For(int fromInclusive, int toExclusive,
   Action<int, ParallelLoopState> body)

В данной форме делегат Action, описывающий тело цикла, определяется следующим образом:

public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2)

Для метода For() обобщенный параметр T1 должен быть типа int, а обобщенный параметр Т2 — типа ParallelLoopState. Всякий раз, когда делегат Action вызывается, текущее состояние цикла передается в качестве аргумента arg2.

Для преждевременного завершения цикла следует воспользоваться методом Break(), вызываемым для экземпляра объекта типа ParallelLoopState внутри тела цикла, определяемого параметром body. Метод Break() объявляется следующим образом:

public void Break()

Вызов метода Break() формирует запрос на как можно более раннее прекращение параллельно выполняемого цикла, что может произойти через несколько шагов цикла после вызова метода Break(). Но все шаги цикла до вызова метода Break() все же выполняются. Следует, также иметь в виду, что отдельные части цикла могут и не выполняться параллельно. Так, если выполнено 10 шагов цикла, то это еще не означает, что все эти 10 шагов представляют 10 первых значений переменной управления циклом.

Прерывание цикла, параллельно выполняемого методом For(), нередко оказывается полезным при поиске данных. Так, если искомое значение найдено, то продолжать выполнение цикла нет никакой надобности. Прерывание цикла может оказаться полезным и в том случае, если во время очередной операции встретились недостоверные данные.

Метод ForEach()

Используя метод ForEach(), можно создать распараллеливаемый вариант цикла foreach. Существует несколько форм метода ForEach(). Ниже приведена простейшая форма его объявления:

public static ParallelLoopResult
    ForEach<TSource>(IEnumerable<TSource> source,
        Action<TSource> body)

где source обозначает коллекцию данных, обрабатываемых в цикле, a body — метод, который будет выполняться на каждом шаге цикла. Как пояснялось ранее, во всех массивах, коллекциях и других источниках данных поддерживается интерфейс IEnumerable<T>. Метод, передаваемый через параметр body, принимает в качестве своего аргумента значение или ссылку на каждый обрабатываемый в цикле элемент массива, но не его индекс. А в итоге возвращаются сведения о состоянии цикла.

Аналогично методу For(), параллельное выполнение цикла методом ForEach() можно остановить, вызвав метод Break() для экземпляра объекта типа ParallelLoopState, передаваемого через параметр body, при условии, что используется приведенная ниже форма метода ForEach():

public static ParallelLoopResult
    ForEach<TSource>(IEnumerable<TSource> source,
        Action<TSource, ParallelLoopState> body)
Пройди тесты
Лучший чат для C# программистов