Основы параллельного программирования с использованием Visual Studio 2010

Parallel LINQ (PLINQ)

Показывать лекцию целиком

Описание LINQ

LINQ (Language-Integrated Query) или язык интегрированных запросов - набор функций, который расширяет возможности синтаксиса языков программирования C# и Visual Basic. Проще говоря, LINQ это язык запросов, который возвращает набор объектов, одиночный объект или подмножество полей из объекта, либо набора объектов. В LINQ этот возвращенный набор называется последовательностью (sequence), последовательности LINQ чаще всего используют тип IEnumerable<T>, где Т - тип данных объектов, находящихся в последовательности (например IEnumerable<string>, IEnumerable<[] int>).

LINQ включает в себя около 50 стандартных операций, которые в свою очередь делятся на 2 группы - отложенные операции (выполняются при вызове) и не отложенные операции (выполняются в первую очередь). На Рис. 11.1 представлена схема данных запросов. (рис 11.1) Схема стандартных операторов используемых LINQ

Полный список методов, которые используются в LINQ-запросах, представлен в Табл. 11.1.

Список методов используемых в LINQ-запросах
Оператор Описание
Объединение
Aggregate Применяет к последовательности пользовательский метод.
Average Вычисляет среднее для числовой последовательности.
Count Возвращает количество элементов в последовательности (целочисленное значение).
LongCount Возвращает количество элементов в последовательности (значение в диапазоне LongInt).
Min Возвращает наименьшее значение для числовой последовательности.
Max Возвращает наибольшее значение для числовой последовательности.
Sum Складывает члены числовой последовательности.
Конкатенация
Concat Соединяет две последовательности в одну.
Преобразование
Cast Преобразует элементы последовательности в элементы указанного типа.
OfType Выбирает из элементов последовательности элементы указанного типа.
ToArray Возвращает массив из элементов последовательности.
ToDictionary Возвращает словарь из элементов последовательности.
ToList Возвращает список из элементов последовательности.
ToLookup Возвращает результаты поиска по последовательности.
ToSequence Возвращает последовательность IEnumerable.
Элемент
DefaultIfEmpty Создает стандартный элемент для пустой последовательности.
ElementAt Возвращает элемент последовательности по указанному индексу.
ElementAtOrDefault Возвращает элемент по указанному индексу или стандартный элемент (если индекс вышел за пределы диапазона).
First Возвращает первый элемент последовательности.
FirstOrDefault Возвращает первый элемент последовательности или стандартный элемент (если нужный элемент не найден).
Last Возвращает последний элемент последовательности.
LastOrDefault Возвращает последний элемент последовательности или стандартный элемент (если нужный элемент не найден).
Single Возвращает единственный элемент последовательности.
SingleOrDefault Возвращает единственный элемент последовательности или стандартный элемент (если нужный элемент не найден).
Равенство
SequenceEqual Проверяет эквивалентность двух последовательностей.
Создание
Empty Создает пустую последовательность.
Range Создает последовательность в соответствии с заданным диапазоном.
Repeat Создает последовательность, повторяя значение заданное количество раз.
Группировка
GroupBy Группирует элементы последовательности указанным образом.
Присоединение
GroupJoin Выполняет группированное соединение двух последовательностей.
Join Выполняет внутреннее соединение двух последовательностей.
Упорядочение
OrderBy Упорядочивает элементы последовательности по заданным значениям в порядке возрастания.
OrderByDescending Упорядочивает элементы последовательности по заданным значениям в порядке убывания.
ThenBy Упорядочивает элементы уже упорядоченной последовательности в порядке возрастания.
ThenByDescending Упорядочивает элементы уже упорядоченной последовательности в порядке убывания.
Reverse Зеркально отображает порядок расположения элементов в последовательности.
Разделение на части
Skip Возвращает последовательность, в которой указанное число элементов пропущено.
SkipWhile Возвращает последовательность, в которой пропущены элементы, не соответствующие указанному условию.
Take Возвращает последовательность, в которую включается указанное число элементов.
TakeWhile Возвращает последовательность, в которую включаются элементы, соответствующие указанному условию.
Проекция
Select Создает проекцию части последовательности.
SelectMany Создает проекцию части последовательности по принципу "один ко многим".
Кванторы
All Определяет соответствие всех элементов последовательности указанным условиям.
Any Определяет, есть ли в последовательность элементы, удовлетворяющие указанным условиям.
Contains Определяет, есть ли в последовательности указанный элемент.
Ограничение
Where Сортирует члены последовательности.
Настройка
Distinct Возвращает последовательность без повторяющихся элементов.
Except Возвращает последовательность, представляющую собой разность двух других последовательностей.
Intersect Возвращает последовательность, представляющую собой пересечение двух других последовательностей.
Union Возвращает последовательность, представляющую собой объединение двух других последовательностей.

Примеры LINQ - запросов

Работа с простыми массивами данных

В первом примере, используем LINQ-запрос для того, что бы из заданного массива выбрать диапазон чисел с условием, что элемент массива будет больше 1, но меньше 9 включительно:

static void Main(string[] args)
        {
            int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
            
            var nums = from i in numbers
                       where i > 1  i <= 9
                       select i;

            foreach (var n in nums)
            {
                Console.Write(n + " ");
            }
            Console.ReadLine();
        } 
(рис 11.2) Результат выполнения LINQ - запроса по выборке чисел

Рассмотрим подробнее строчку с LINQ-запросом:

var nums = from i in numbers
                       where i > 1  i <= 9
                       select i;

nums - это неявный тип переменной, которой присваивается результат выполнения LINQ -запроса; i - это переменная диапазона; numbers - источник данных.

Этот же LINQ - запрос (как и любой другой) можно реализовать с помощью лямбда-выражения:

var nums = numbers.Where(i => i > 1  i <= 9);

Также, в фрагменте кода с оператором select, можно вызывать функцию или реализованный ранее метод, и тогда переменной nums - будет присвоен результат выполнения функции или метода. Как показано в примере ниже переменная nums будет содержать результаты работы функции Math.Sqrt которая вычисляет квадратный корень из массива numbers:

var nums = from i in numbers
                       where i > 1  i <= 9
                       select Math.Sqrt(i);
(рис 11.3) Результат выполнения LINQ - запроса с использованием функции Math.Sqrt()

Работа с массивами и коллекциями классов

Также, с помощью LINQ - запросов можно обращаться к другим источникам данных, таким как типизированные списки List<T>, объекты, которых содержат простые экземпляры классов. В нашем случае - это объекты класса Employee:

public  class Employee
{
public int id {get;set;}
public string firstname { get; set; }
public string lastname {get;set;}
public double salary {get;set;}

       public static List<Employee> GetEmployees()
         {
             List<Employee> emp = new List<Employee>
           {
           
            new Employee { id = 1, firstname = "Иван", lastname = "Иванов",salary=3500 },
            new Employee { id = 2, firstname = "Сергей", lastname = "Сидоров", salary = 1710 },
            new Employee { id = 3, firstname = "Алексей", lastname = "Петренко", salary = 2115.3 },
            new Employee { id = 4, firstname = "Александр", lastname = "Иванов", salary = 1411.1 }
            };
            return (emp);
        }
}
        static void Main(string[] args)
        {
            var employees = Employee.GetEmployees();


            var employee = from i in employees
                      where i.lastname=="Иванов"
                      select i;

            foreach (var n in employee)
            {
                Console.WriteLine(n.id + " " + n.firstname +" "+n.lastname +" "+ n.salary);
           }
            Console.ReadLine();
        } 

В методе Main() получаем список сотрудников с помощью метода GetEmployees() который возвращает список сотрудников, содержащийся в типизированной коллекции List<Employee>. LINQ-запрос получает список сотрудников, у которых фамилия "Иванов" (Рис. 11.4)

(рис 11.4) Результат выполнения LINQ - запроса с использованием объектов класса Employee

Введение в PLINQ

PLINQ (Параллельный LINQ) - это распараллеленная реализация LINQ. Запросы PLINQ схожи с не параллельными запросами LINQ, основное отличие этих запросов заключается в том, что PLINQ полностью использует возможности всех процессоров в системе. Результаты запросов PLINQ, также как и последовательные запросы LINQ, возвращают тип IEnumerable<T>, данный результат достигается путем разделения источника данных на сегменты и параллельной обработки запроса каждого сегмента в отдельном рабочем потоке на нескольких процессорах.

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

Все основные методы PLINQ содержаться в классе System.Linq.ParallelEnumerable - пространства имен System.Linq компилируются в сборку System.Core.dll. Данный класс включает в себя, реализации всех стандартных операторов запросов, поддерживаемых LINQ to Objects.

Кроме стандартных операторов-запросов класс ParallelEnumerable содержит набор методов, которые обеспечивают поведения, характерные для параллельного выполнения. Эти методы представлены в Табл. 11.2.

Основные методы класса ParallelEnumerable
Методы класса ParallelEnumerable Описание
AsParallel Точка входа для PLINQ. Указывает на необходимость параллелизации остальной части запроса, если это возможно.
AsSequential<TSource> Указывает на необходимость последовательного выполнения остальной части запроса как непараллельного запроса LINQ.
AsOrdered Указывает, что PLINQ должен сохранить порядок исходной последовательности для остальной части запроса или до тех пор, пока порядок не будет изменен.
AsUnordered<TSource> Указывает, что PLINQ не должен сохранять порядок исходной последовательности для остальной части запроса.
WithCancellation<TSource> Указывает, что PLINQ должен периодически отслеживать состояние предоставленного токена отмены и отменять выполнение при запросе.
WithDegreeOfParallelism<TSource> Указывает максимальное количество процессоров, которое должен использовать PLINQ для параллелизации запроса.
WithMergeOptions<TSource> Предоставляет подсказку о том, как PLINQ должен, если это возможно, выполнять слияние параллельных результатов в одну последовательность в потоке-потребителе.
WithExecutionMode<TSource> Указывает, должен ли PLINQ выполнять параллелизацию запроса, даже если согласно поведению по умолчанию он будет выполняться последовательно.
ForAll<TSource> Многопоточный метод перечисления, который в отличие от итерации результатов запроса позволяет обрабатывать результаты параллельно без предварительного слияния в поток-потребитель.
Перегрузка Aggregate Уникальная для PLINQ перегрузка, обеспечивающая промежуточное агрегирование локальных частей потока, и предоставляющая функцию окончательного агрегирования для объединения результатов всех частей.

Метод AsParallel

Метод AsParallel() - это некая входная точка в использовании запросов PLINQ. Он преобразует последовательность данных в ParallelQuery. Механизм LINQ обнаруживает использование ParallelQuery в качестве источника в запросе и переключается на выполнение PLINQ автоматически. Метод AsParallel() несколько типов перегрузки, которые представлены ниже:

  • Первый тип работает с использованием IEnumerable <TSource> и возвращает экземпляр ParallelQuery <TSource>, который может быть использован в качестве основы запроса PLINQ:
    public static ParallelQuery<TSource> AsParallel<TSource>(
    this IEnumerable<TSource> source 
    )
  • Второй тип создает экземпляр ParallelQuery из IEnumerable и предназначен для поддержки унаследованных коллекций, таких как System.Collections.ArrayList. Объект ParallelQuery не является строго типизированным и не может использоваться в качестве основы запроса PLINQ без преобразования в ParallelQuery:
    public static ParallelQuery AsParallel(
    this IEnumerable source
    )
  • Оба типа возвращают исключение вида ArgumentNullException.

    Пример использования PLINQ запроса

    Что бы преобразовать LINQ в PLINQ, в запросе LINQ достаточно вызвать метод AsParallel() в LINQ запросе. В следующем примере находятся числа в диапазоне от 1 до 100000, кратные двум с помощью неоптимизированного параллельного алгоритма:

    var source = Enumerable.Range(1, 100000);
                var parallelQuery = from num in source.AsParallel()
                                    where num % 2 == 0
                                    select num;
      foreach (var n in parallelQuery)
                {
                    Console.WriteLine(n);
                }
                Console.ReadLine();

    Этот же запрос можно было записать с использованием лямда-выражения:

    var parallelQuery = source.AsParallel().Where(n => n % 2 == 0).Select(n => n);
    (рис 11.5) Результаты выполнения запроса PLINQ

    Существует ряд причин, по которым, PLINQ не используется по умолчанию. Первое, использование PLINQ целесообразно использовать при наличии значительного количества вычислительных задач, распределенных по рабочим потокам. Многие запросы LINQ выполняются очень быстро, и распараллеливание не только не нужно, но и нецелесообразно, так как накладные расходы на разделение данных, объединения результатов и координацию дополнительных потоков могут, на самом деле, только ухудшить производительность. В следующем списке описаны формы запросов, которые PLINQ по умолчанию будет выполнять в последовательном режиме:

  • Запросы, которые содержат индексированный Where, индексированный SelectMany или ElementAt после оператора упорядочивания или фильтрации, который удаляет или переупорядочивает исходные индексы.
  • Запросы, которые содержат оператор Take, TakeWhile, Skip, SkipWhile и в которых индексы в исходной последовательности не находятся в исходном порядке.
  • Запросы, которые содержат Zip или SequenceEquals за исключением, когда один из источников данных содержит изначально упорядоченный индекс, а другие источники данных можно проиндексировать (то есть массив или IList(T)).
  • Запросы, которые содержат Concat, за исключением случая, если они применяются к индексируемым источникам данных.
  • Запросы, которые содержат Reverse, за исключением случая, если они применяются к индексируемым источникам данных.
  • Вернуться к учебному плану