Главная страница

программирование. Руководство su P# a n Reference в herbert schildt полное руководство с 0 герберт шилдт


Скачать 3.32 Mb.
НазваниеРуководство su P# a n Reference в herbert schildt полное руководство с 0 герберт шилдт
Анкорпрограммирование
Дата25.01.2022
Размер3.32 Mb.
Формат файлаrtf
Имя файлаc-40-polnoe-rukovodstvo-2011.rtf
ТипРуководство
#341448
страница80 из 97
1   ...   76   77   78   79   80   81   82   83   ...   97

В приведенном ниже примере программы демонстрируется применение метода Invoke () на практике. В этой программе два метода MyMeth () и MyMeth2 () выполняются параллельно посредством вызова метода Invoke () . Обратите внимание на простоту организации данного процесса.

// Применить метод Parallel.Invoke() для параллельного выполнения двух методов.

using System;

using System.Threading;

using System.threading.Tasks;

class DemoParallel {

// Метод, исполняемый как задача, static void MyMeth() {

Console.WriteLine("MyMeth запущен");

for (int count = 0; count < 5; count++) {

Thread.Sleep(500);

Console.WriteLine("В методе MyMeth подсчет равен " + count );

}

Console.WriteLine("MyMeth завершен");

}

// Метод, исполняемый как задача, static void MyMeth2() {

Console.WriteLine("MyMeth2 запущен");

for(int count = 0; count < 5; count++) {

Thread.Sleep(500);

Console.WriteLine("В методе MyMeth2, подсчет равен " + count );

}

Console.WriteLine("MyMeth2 завершен");

}

static void Main() {

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

// Выполнить параллельно два именованных метода.

Parallel.Invoke(MyMeth, MyMeth2);

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

}

}

Выполнение этой программы может привести к следующему результату.

Основной поток запущен.

MyMeth() запущен MyMeth2() запущен В методе MyMeth() подсчет равен 0 В методе MyMeth2() подсчет равен 0 В методе MyMeth() подсчет равен 1 В методе MyMeth2() подсчет равен 1 В методе MyMeth() подсчет равен 2 В методе MyMeth2() подсчет равен 2 В методе MyMeth() подсчет равен 3

В методе MyMeth2() подсчет равен 3 В методе MyMethO подсчет равен 4 MyMeth() завершен

В методе MyMeth2() подсчет равен 4 MyMeth2() завершен Основной поток завершен.

В данном примере особое внимание обращает на себя следующее обстоятельство: выполнение метода Main () приостанавливается до тех пор, пока не произойдет возврат из метода Invoke (). Следовательно, метод Main (), в отличие от методов MyMeth () и MyMeth2 (), не выполняется параллельно. Поэтому применять метод Invoke () показанным здесь способом нельзя в том случае, если требуется, чтобы исполнение вызывающего потока продолжалось.

В приведенном выше примере использовались именованные методы, но для вызова метода Invoke () это условие не является обязательным. Ниже приведен переделанный вариант той же самой программы, где в качестве аргументов в вызове метода Invoke () применяются лямбда‑выражения.

// Применить метод Parallel.Invoke() для параллельного выполнения двух методов. // В этой версии программы применяются лямбда‑выражения.

using System;

using System.Threading;

using System.Threading.Tasks;

class DemoParallel {

static void Main() {

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

// Выполнить два анонимных метода, указываемых в лямбда‑выражениях.

Parallel.Invoke( () => {

Console.WriteLine("Выражение #1 запущено");

for(int count = 0; count < 5; count++) {

Thread.Sleep(500);

Console.WriteLine("В выражении #1 подсчет равен " + count );

}

Console.WriteLine("Выражение #1 завершено");

},

О => {

Console.WriteLine("Выражение #2 запущено");

for (int count = 0; count < 5; count++) {

Thread.Sleep(500);

Console.WriteLine("В выражении #2 подсчет равен " + count );

}

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

}

}

Эта программа дает результат, похожий на результат выполнения предыдущей программы.

Применение метода For ()

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

public static ParallelLoopResult

For(int fromlnclusive, int toExclusive, Action body)

где fromlnclusive обозначает начальное значение того, что соответствует переменной управления циклом; оно называется также итерационным, или индексным, значением; a toExclusive – значение, на единицу больше конечного. На каждом шаге цикла переменная управления циклом увеличивается на единицу. Следовательно, цикл постепенно продвигается от начального значения fromlnclusive к конечному значению toExclusive минус единица. Циклически выполняемый код указывается методом, передаваемым через параметр body. Этот метод должен быть совместим с делегатом Action, объявляемым следующим образом.

public delegate void Action(T obj)

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

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

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

11 Применить метод Parallel.For() для организации параллельно // выполняемого цикла обработки данных.

using System;

using System.Threading.Tasks;

class DemoParallelFor { static int[] data;

// Метод, служащий в качестве тела параллельно выполняемого цикла.

// Операторы этого цикла просто расходуют время ЦП для целей демонстрации, static void MyTrlansform (int i) { data[i] = data[i] / 10;

if(data[i] < 10000) data[i] = 0;

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

}

static void Main() {

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

data = new int[100000000];

// Инициализировать данные в обычном цикле for. for (int i=0; i < data.Length; i++) data[i] = i;

// Распараллелить цикл методом For().

Parallel.For(0, data.Length, MyTransform);

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

}

}

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

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

создаются последовательный и параллельный варианты цикла for, а для сравнения на экран выводится время выполнения каждого из них.

// Продемонстрировать отличия во времени последовательного //и параллельного выполнения цикла for.

using System;

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

class DemoParallelFor { static int[] data;

// Метод, служащий в качестве тела параллельно выполняемого цикла.

// Операторы этого цикла просто расходуют время ЦП для целей демонстрации, static void MyTransform(int i) { data[i] = data[i] / 10;

if(data[i] < 1000) data[i] = 0;

if(data[i] > 1000 & data[i] < 2000) data[i] = 100; if(data[i] > 2000 & data[i] < 3000) data[i] = 200; if(data[i] > 3000) data[i] = 300;

}

static void Main() {

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

// Create экземпляр объекта типа Stopwatch // для хранения времени выполнения цикла.

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.Start();

// Параллельный вариант преобразования данных в цикле.

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("Основной поток завершен.");

}

}

При выполнении этой программы на двухъядерном компьютере получается следующий результат.

Основной поток запущен.

Параллельно выполняемый цикл инициализации: 1.0537757 секунд Последовательно выполняемый цикл инициализации: 0.3457628 секунд

Параллельно выполняемый цикл преобразования: 4.2246675 секунд Последовательно выполняемый цикл преобразования: 5.3849959 секунд Основной поток завершен.

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

ПРИМЕЧАНИЕ

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

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

Parallel.For(0, data.Length, (i) => data[i] = i );

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

И во‑вторых, обратите внимание на применение класса Stopwatch для вычисления времени выполнения цикла. Этот класс находится в пространстве имен System. Diagnostics. Для того чтобы воспользоваться им, достаточно создать экземпляр его объекта, а затем вызвать метод Start () , начинающий отчет времени, и далее – метод Stop (), завершающий отсчет времени. А с помощью метода Reset () отсчет времени сбрасывается в исходное состояние. Продолжительность выполнения можно получить различными способами. В рассматриваемой здесь программе для этой цели использовано свойство Elapsed, возвращающее объект типа TimeSpan. С помощью этого объекта и свойства TotalSeconds время отображается в секундах, включая и доли секунды. Как показывает пример рассматриваемой здесь программы, класс Stopwatch оказывается весьма полезным при разработке параллельно исполняемого кода.

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

public bool IsCompleted { get; }

public Nullable LowestBreaklteration { get; }

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

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

public static ParallelLoopResult For(int fromlnclusive, int toExclusive,

Action body)

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

public delegate void Action(T argl, T2 arg2)

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

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

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

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

В приведенном ниже примере программы демонстрируется применение метода Break () для прерывания цикла, параллельно выполняемого методом For () . Это вариант предыдущего примера, переработанный таким образом, чтобы метод MyTransform () принимал теперь объект типа ParallelLoopState в качестве своего параметра, а метод Break () вызывался при обнаружении отрицательного значения в массиве данных. Отрицательное значение, по которому прерывается выполнение цикла, вводится в массив data внутри метода Main () . Далее проверяется состояние завершения цикла преобразования данных. Свойство IsCompleted будет содержать логическое значение false, поскольку в массиве data обнаруживается отрицательное значение. При этом на экран выводится номер шага, на котором цикл был прерван. (В этой программе исключены все избыточные циклы, применявшиеся в ее предыдущей версии, а оставлены только самые эффективные из них: последовательно выполняемый цикл инициализации и параллельно выполняемый цикл преобразования.)

// Использовать объекты типа ParallelLoopResult и ParallelLoopState, а также А/ метод Break() вместе с методом For() для параллельного выполнения цикла.

using System;

using System.Threading.Tasks;

class DemoParallelForWithLoopResult { static int[] data;

// Метод, служащий в качестве тела параллельно выполняемого цикла.

// Операторы этого цикла просто расходуют время ЦП для целей демонстрации, static void MyTransform(int i, ParallelLoopState pis) {

// Прервать цикл при обнаружении отрицательного значения, if(data[i] < 0) pis.Break();

data[i] = data[i] / 10;

if(data[i] < 1000) data[i] = 0;

if(data[i] > 1000 & data[i] < 2000) data[i] = 100; if(data[i] > 2000 & data[i] < 3000) data[i] = 200; if(data[i] > 3000) data[i] = 300;

}

static void Main() {

Console.WriteLine("Основной поток запущен."); data = new int[100000000];

// Инициализировать данные.

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

// Поместить отрицательное значение в массив data, data[1000] = ‑10;

// Параллельный вариант инициализации массива в цикле.

ParallelLoopResult loopResult = Parallel.For(0, data.Length, MyTransform);

// Проверить, завершился ли цикл, if(!loopResult.IsCompleted)

Console.WriteLine("\пЦикл завершился преждевременно из‑за того, " +

"что обнаружено отрицательное значение\п" +

"на шаге цикла номер " +

loopResult.LowestBreaklteration + ".\n");

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

}

}

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

Основной поток запущен.

Цикл завершился преждевременно из‑за того, что обнаружено отрицательное значение на шаге цикла номер 1000

Основной поток завершен.

Как следует из приведенного выше результата, цикл преобразования данных преждевременно завершается после 1000 шагов. Дело в том, что метод Break () вызывается внутри метода MyTransform () при обнаружении в массиве данных отрицательного значения.

Помимо двух описанных выше форм метода For () существует и ряд других его форм. В одних из этих форм допускается указывать различные дополнительные параметры, а в других – использовать параметры типа long вместо int для пошагового выполнения цикла. Имеются также формы метода For () , предоставляющие такие дополнительные преимущества, как, например, возможность указывать метод, вызываемый по завершении потока каждого цикла.

И еще одно, последнее замечание: если требуется остановить цикл, параллельно выполняемый методом For () , не обращая особого внимания на любые шаги цикла, которые еще могут быть в нем выполнены, то для этой цели лучше воспользоваться методом Stop () , чем методом Break () .

Применение метода ForEach ()

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

public static ParallelLoopResult

ForEach(IEnumerable source,

Action body)

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

о состоянии цикла.

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

public static ParallelLoopResult

ForEach(IEnumerable source,

ActiorKTSource, ParallelLoopState> body)

В приведенном ниже примере программы демонстрируется применение метода For Each () на практике. Как и прежде, в данном примере создается крупный массив целых значений. А отличается данный пример от предыдущих тем, что метод, выполняющийся на каждом шаге цикла, просто выводит на консоль значения из массива. Как правило, метод WriteLine () в распараллеливаемом цикле не применяется, потому что ввод‑вывод на консоль осуществляется настолько медленно, что цикл оказывается полностью привязанным к вводу‑выводу. Но в данном примере метод WriteLine () применяется исключительно в целях демонстрации возможностей метода ForEach (). При обнаружении отрицательного значения выполнение цикла прерывается вызовом метода Break (). Несмотря на то что метод Break () вызывается в одной задаче, другая задача может по‑прежнему выполняться в течение нескольких шагов цикла, прежде чем он будет прерван, хотя это зависит от конкретных условий работы среды выполнения.

// Использовать объекты типа ParallelLoopResult и ParallelLoopState, а также // метод Break () вместе с методом ForEachO для параллельного выполнения цикла.

using System;

using System.Threading.Tasks;

class DemoParallelForWithLoopResult { static int[] data;

// Метод, служащий в качестве тела параллельно выполняемого цикла.

// В данном примере переменной v передается значение элемента массива // данных, а не индекс этого элемента.

static void DisplayData(int v, ParallelLoopState pis) {

// Прервать цикл при обнаружении отрицательного значения, if (v < 0) pls.Break();

Console.WriteLine("Значение: " + v);

static void Main() {

Console.WriteLine("Основной поток запущен."); data = new int[100000000];

// Инициализировать данные.

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

// Поместить отрицательное значение в массив data, data[100000] = ‑10;

// Использовать цикл, параллельно выполняемый методом ForEachO,

// для отображения данных на экране.

ParallelLoopResult loopResult = Parallel.ForEach(data, DisplayData);

// Проверить, завершился ли цикл, if(!loopResult.IsCompleted)

Console.WriteLine("ХпЦикл завершился преждевременно из‑за того, " +

"что обнаружено отрицательное значение\п" +

"на шаге цикла номер " +

loopResult.LowestBreaklteration + ".\n");

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

}

}

В приведенной выше программе именованный метод применяется в качестве делегата, представляющего "тело" цикла. Но иногда удобнее применять анонимный метод. В качестве примера ниже приведено реализуемое в виде лямбда‑выражения "тело" цикла, параллельно выполняемого методом ForEach ().

// Использовать цикл, параллельно выполняемый методом ForEachO,

// для отображения данных на экране.

ParallelLoopResult loopResult =

Parallel.ForEach(data, (v, pis) => {

Console.WriteLine("Значение: " + v); if (v < 0) pis.Break ();

});

Исследование возможностей PLINQ

PLINQ представляет собой параллельный вариант языка интегрированных запросов LINQ и тесно связан с библиотекой TPL. PLINQ применяется, главным образом, для достижения параллелизма данных внутри запроса. Как станет ясно из дальнейшего, сделать это совсем не трудно. Как и TPL, тема PLINQ довольно обширна и многогранна, поэтому в этой главе представлены лишь самые основные понятия данного языка.

Класс ParallelEnumerable

Основу PLINQ составляет класс ParallelEnumerable, определенный в пространстве имен System. Linq. Это статический класс, в котором определены многие методы расширения, поддерживающие параллельное выполнение операций. По существу, он представляет собой параллельный вариант стандартного для LINQ класса Enumerable. Многие его методы являются расширением класса ParallelQuery, а некоторые из них возвращают объект типа ParallelQuery. В классе ParallelQuery инкапсулируется последовательность операций, поддерживающая параллельное выполнение. Имеются как обобщенный, так и необобщенный варианты данного класса. Мы не будем обращаться к классу ParallelQuery непосредственно, а воспользуемся несколькими методами класса ParallelEnumerable. Самый главный из них, метод As Parallel () , описывается в следующем разделе.

Распараллеливание запроса методом AsParallel ()

Едва ли не самым удобным средством PLINQ является возможность просто создавать параллельный запрос. Нужно лишь вызвать метод AsParallel () для источника данных. Метод AsParallel () определен в классе ParallelEnumerable и возвращает источник данных, инкапсулированный в экземпляре объекта типа ParallelQuery. Это дает возможность поддерживать методы расширения параллельных запросов. После вызова данного метода запрос разделяет источник данных на части и оперирует с каждой из них таким образом, чтобы извлечь максимальную выгоду из распараллеливания. (Если распараллеливание оказывается невозможным или неприемлемым, то запрос, как обычно, выполняется последовательно.) Таким образом, добавления в исходный код единственного вызова метода AsParallel () оказывается достаточно для того, чтобы превратить последовательный запрос LINQ в параллельный запрос LINQ. Для простых запросов это единственное необходимое условие.

Существуют как обобщенные, так и необобщенные формы метода AsParallel (). Ниже приведена простейшая обобщенная его форма:

public static ParallelQuery AsParallel(this IEnumerable source) public static ParallelQuery

AsParallel(this IEnumerable source)

где TSource обозначает тип элементов в последовательном источнике данных

source.

Ниже приведен пример, демонстрирующий простой запрос PLINQ.

// Простой запрос PLINQ.

using System; using System.Linq;

class PLINQDemo {

static void Main() {

int[] data = new int[10000000];

I

// Инициализировать массив данных положительными значениями, for(int i=0; i < data.Length; i++) data[i] = i;

//А теперь ввести в массив данных ряд отрицательных значений

data[1000] = ‑1;

data[14000] = ‑2;

data[15000] = ‑3;

data[676000] = ‑4;

data[8024540] = ‑5; data[9908000] = ‑6;

// Использовать запрос PLINQ для поиска отрицательных значений, var negatives = from val in data.AsParallel() where val < 0 select val;

foreach(var v in negatives)

Console.Write(v + " ");

Console.WriteLine();

}

}

Эта программа начинается с создания крупного массива data, инициализируемого целыми положительными значениями. Затем в него вводится ряд отрицательных значений. А далее формируется запрос на возврат последовательности отрицательных значений. Ниже приведен этот запрос.

var negatives = from val in data.AsParallel() where val < 0 select val;

В этом запросе метод AsParallel () вызывается для источника данных, в качестве которого служит массив data. Благодаря этому разрешается параллельное выполнение операций над массивом data, а именно: поиск отрицательных значений параллельно в нескольких потоках. По мере обнаружения отрицательных значений они добавляются в последовательность вывода. Это означает, что порядок формирования последовательности вывода может и не отражать порядок расположения отрицательных значений в массиве data. В качестве примера ниже приведен результат выполнения приведенного выше кода в двухъядерной системе.

5 ‑6 ‑1 ‑2 ‑3 ‑4

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

Применение метода AsOrdered ()

Как отмечалось в предыдущем разделе, по умолчанию порядок формирования результирующей последовательности в параллельном запросе совсем не обязательно должен отражать порядок формирования исходной последовательности. Более того, результирующую последовательность следует рассматривать как практически неупорядоченную. Если же результат должен отражать порядок организации источника данных, то его нужно запросить специально с помощью метода AsOrdered () , определенного в классе ParallelEnumerable. Ниже приведены обобщенная и необобщенная формы этого метода:

public static ParallelQuery AsOrdered(this ParallelQuery source) public static ParallelQuery

AsOrdered(this ParallelQuery source)

где TSource обозначает тип элементов в источнике данных source. Метод AsOrdered () можно вызывать только для объекта типа ParallelQuery, поскольку он является методом расширения класса ParallelQuery.

Для того чтобы посмотреть, к какому результату может привести применение метода AsOrdered () , подставьте его вызов в приведенный ниже запрос из предыдущего примера программы.

// Использовать метод AsOrdered() для сохранения порядка // в результирующей последовательности.

var negatives = from val in data.AsParallel().AsOrdered() where val < 0 select val;

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

Отмена параллельного запроса

Параллельный запрос отменяется таким же образом, как и задача. И в том и в другом случае отмена опирается на структуру CancellationToken, получаемую из класса CancellationTokenSource. Получаемый в итоге признак отмены передается запросу с помощью метода WithCancellation () . Отмена параллельного запроса производится методом Cancel () , который вызывается для источника признаков отмены. Главное отличие отмены параллельного запроса от отмены задачи состоит в следующем: когда параллельный запрос отменяется, он генерирует исключение OperationCanceledException, а не AggregateException. Но в тех случаях, когда запрос способен сгенерировать несколько исключений, исключение OperationCanceledException может быть объединено в совокупное исключение AggregateException. Поэтому отслеживать лучше оба вида исключений.

Ниже приведена форма объявления метода WithCancellation ():

public static ParallelQuery

WithCancellation (

this ParallelQuery source,

CancellationToken cancellationToken)

где source обозначает вызывающий запрос, a cancellationToken – признак отмены. Этот метод возвращает запрос, поддерживающий указанный признак отмены.

В приведенном ниже примере программы демонстрируется порядок отмены параллельного запроса, сформированного в программе из предыдущего примера. В данной программе организуется отдельная задача, которая ожидает в течение 100 миллисекунд, а затем отменяет запрос. Отдельная задача требуется потому, что цикл foreach, в котором выполняется запрос, блокирует выполнение метода Main () до завершения цикла.

using System.Linq; using System.Threading; using System.Threading.Tasks;

class PLINQCancelDemo {

static void Main() {

CancellationTokenSource cancelTokSrc = new CancellationTokenSource(); int[] data = new int[10000000];

// Инициализировать массив данных положительными значениями, for (int i=0; i < data.Length; i++) data[i] = i;

//А теперь ввести в массив данных ряд отрицательных значений, data[1000] = ‑1; data [14000] = ‑2; data[15000] = ‑3;

data[676000] = ‑4;    ч

data[8024540] = ‑5; data [9908000] = ‑6;

// Использовать запрос PLINQ для поиска отрицательных значений, var negatives = from val in data.AsParallel().

WithCancellation(cancelTokSrc.Token) where val < 0 select val;

// Создать задачу для отмены запроса по истечении 100 миллисекунд.

Task cancelTsk = Task.Factory.StartNew( () => {

Thread.Sleep(100); • cancelTokSrc.Cancel();

});

try {

foreach(var v in negatives)

Console.Write(v + " ");

} catch(OperationCanceledException exc) {

Console.WriteLine(exc.Message);

} catch(AggregateException exc) {

Console.WriteLine (exc);

} finally {

cancelTsk.Wait (); cancelTokSrc.Dispose(); cancelTsk.Dispose();

}

Console.WriteLine();

}

}

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

Запрос отменен с помощью маркера, переданного в метод WithCancellation.

Другие средства PLINQ

Как упоминалось ранее, PLINQ представляет собой довольно крупную подсистему. Это объясняется отчасти той гибкостью, которой обладает PLINQ. В PLINQ доступны и многие другие средства, помогающие подстраивать параллельные запросы под конкретную ситуацию. Так, при вызове метода WithDegreeOf Parallelism () можно указать максимальное количество процессоров, выделяемых для обработки запроса, а при вызове метода As Sequential ( ) – запросить последовательное выполнение части параллельного запроса. Если вызывающий поток, ожидающий результатов от цикла foreach, не требуется блокировать, то для этой цели можно воспользоваться методом ForAll (). Все эти методы определены в классе ParallelEnumerable. А в тех случаях, когда PLINQ должен по умолчанию поддерживать последовательное выполнение, можно воспользоваться методом WithExecutionMode () , передав ему в качестве параметра признак ParallelExecutionMode . ForceParallelism.
Вопросы эффективности PLINQ

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

1   ...   76   77   78   79   80   81   82   83   ...   97


написать администратору сайта