Запросы Parallel LINQ

68

По большей части использование Parallel LINQ, обычно называемого PLINQ, очень похоже на применение LINQ to Object. Фактически это одна из привлекательных сторон PLINQ. В обычном запросе LINQ to Query источником данных является IEnumerable<T>, где T — обрабатываемый тип данных. Механизм LINQ автоматически переключается на использование PLINQ, когда источником данных является экземпляр типа ParallelQuery<T>. И здесь есть один трюк: любой IEnumerable<T> может быть преобразован в ParallelQuery<T> просто с использованием метода AsParallel. Давайте рассмотрим код. Ниже показаны запросы LINQ to Object и PLINQ, которые делают одно и то же:

string[] cars = { "Nissan", "Aston Martin", "Chevrolet", "Alfa Romeo", "Chrysler", "Dodge", "BMW",
                              "Ferrari", "Audi", "Bentley", "Ford", "Lexus", "Mercedes", "Toyota", "Volvo", "Subaru", "Жигули :)"};

            // Последовательный запрос LINQ
            IEnumerable<string> auto = cars.Where(p => p.Contains("s"));

            foreach (string s in auto)
                Console.WriteLine("Результат последовательного запроса: " + s);

            // Запрос Parallel LINQ
            auto = cars.AsParallel()
                .Where(p => p.Contains("s"));

            foreach (string s in auto)
                Console.WriteLine("Результат параллельного запроса: " + s);

Первый запрос использует обычный LINQ to Objects для обработки каждой машины с целью нахождения названий, содержащих букву "s". В качестве результата получается IEnumerable<string> и все подходящие имена выводятся на консоль.

Второй запрос делает то же самое, но вдобавок вызывается метод AsParallel. С его помощью источник данных преобразуется в ParallelQuery, что автоматически подразумевает применение Parallel LINQ. И как явно следует из кода, никаких других изменений не требуется. Просто вызвав AsParallel, мы получаем PLINQ:

Сравнение запросов LINQ и Parallel LINQ

Ключевой момент, который важно было продемонстрировать — это простота создания запроса PLINQ. Достаточно просто вызвать метод AsParallel с выражениями запроса или воспользоваться расширяющими методами для структурирования запроса.

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

Предохранение порядка результатов

Данные, представленные в качестве источника для запроса PLINQ, разбиваются на части и разделяются для параллельной обработки. Несколько разделов могут обрабатываться одновременно. Однако каждый из этих разделов обрабатывается последовательно. Задумайтесь об этом - параллельное выполнение происходит из последовательной обработки нескольких разделов данных одновременно. Это показано на рисунке:

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

Таким образом, продолжая пример, первое ядро проверит Nissan на содержание буквы s. Затем будет выполнена проверка Chrysler, Audi, Mercedes. Пока это происходит, второе ядро проверяет AstonMartin, Dodge, и т.д. В то же время третье и четвертое ядра обрабатывают свои разделы.

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

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

Иногда порядок результата не важен. Например, если необходимо только узнать, сколько названий машин содержат букву "s", порядок формирования результата значения не имеет. Однако бывают случаи, когда приходится заботиться о порядке результата. Это особенно верно при преобразовании существующих запросов LINQ в PLINQ. Например, где-то могут существовать предположения о порядке результатов. Чтобы сохранить порядок, необходимо воспользоваться расширяющим методом AsOrdered на объекте ParallelQuery, который создан посредством метода AsParallel.

Давайте рассмотрим пример:

string[] cars = { "Nissan", "Aston Martin", "Chevrolet", "Alfa Romeo", "Chrysler", "Dodge", "BMW",
                              "Ferrari", "Audi", "Bentley", "Ford", "Lexus", "Mercedes", "Toyota", "Volvo", "Subaru", "Жигули :)"};

            // Запрос Parallel LINQ с сохранением порядка
            IEnumerable<string> auto = cars.AsParallel().AsOrdered()
                .Where(p => p.Contains("a"));

            foreach (string s in auto)
                Console.WriteLine(s);

Для расширяющих методов AsParallel и AsOrdered ключевых слов выражений запросов не предусмотрено. Эти методы должны вызываться непосредственно. В приведенном примере ключевые слова запросов смешаны с вызовами расширяющих методов. Если скомпилировать и запустить код, получится следующий результат:

Предохранение порядка результатов запроса PLINQ методом AsOrdered

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

Лучший чат для C# программистов