28 ноября 2008

Parallel Extensions для .net 3.5

.NET
Aquafresh :-)Количество ядер у процессоров растет год от года. Но многие программы до сих пор умеют использовать только одно. В небольшой заметке хочу рассказать о дополнении к библиотеке System.Threading, которое называется Parallel Extensions. Это дополнение позволяет на высоком уровне выполнять задачи на всех доступных ядрах/процессорах.

Данная статья является лишь кратким вводным обзором в Parallel Extensions. Так же в конце статьи вы найдете ссылки на ресурсы, которые раскрывают тему во всех деталях.

Если интересно, то смело ныряем под кат.

Немного истории


Библиотека Parallel Extensions (PE) — совместный проект команды .net и Microsoft Research — впервые увидела свет 29 ноября 2007 года. Она создана для того, чтобы разработчики могли пользоваться современными многоядерными архитектурами, не утруждая себя трудоемким управлением потоками. Программы, написанные с применением библиотеки, автоматически используют все доступные ядра системы. Если же программа будет запущена на старом одноядерном компьютере, то выполнение будет происходить последовательно, практически без потерь в производительности. Таким образом, использование PE раскрывает все преимущества многоядерных технологий, сохраняя работоспособность на одноядерных системах.

Последнее обновление библиотеки было в июне 2008 года. Сейчас она имеет статус Community Technology Preview и, скорее всего, войдет в 4 версию .net.

Состав библиотеки


PE состоит из трех основных компонентов:
  • Task Parallel Library (TPL) — предоставляет такие императивные методы, как Parallel.For, Parallel.Foreach и Parallel.Invoke для выполнения параллельных вычислений. Вся работа по созданию и завершению потоков, в зависимости от имеющихся процессоров выполняется библиотекой автоматически.
  • Parallel LINQ (PLINQ) — надстройка над LINQ to Objects и LINQ to XML, позволяющая выполнять параллельные запросы. В большинстве случаев достаточно в начале запроса написать AsParallel() для того, чтобы все последующие операторы выполнялись параллельно. Внутренне использует TPL.
  • Coordination Data Structures (CDS) — набор структур, который используется для синхронизации и координации выполнения параллельных задач. Ее мы рассматривать в этой статье не будем.

Хочу предупредить, что эта библиотека не решает вопросов, связанных с потоковой безопасностью объектов. Если вы используете не ThreadSafe объекты, то сами должны следить за тем, чтобы объект использовался только одним потоком.

Что будем делать


В качестве примера предлагаю взять поиск простых чисел от 2 до заданного предела. Будем все числа из этого интервала проверять следующим статическим методом:

public class Prime<br> {<br> /// <summary><br> /// Determines whether the specified candidate is prime.<br> /// </summary><br> /// <param name="candidate">The candidate.</param><br> /// <returns><br> /// <c>true</c> if the specified candidate is prime; otherwise, <c>false</c>.<br> /// </returns><br> public static bool IsPrime(int candidate)<br> {<br> for(int d = 2; d <= candidate/2; d++)<br> {<br> if(candidate%d == 0)<br> return false;<br> }<br> return candidate > 1;<br> }<br> }<br><br>* This source code was highlighted with Source Code Highlighter.

Для удобства создадим интерфейс:

interface IPrimeFinder<br> {<br> /// <summary><br> /// Finds primes up to the specified limit.<br> /// </summary><br> /// <param name="limit">The limit.</param><br> /// <returns>List of primes</returns><br> List<int> Find ( int limit );<br> }<br><br>* This source code was highlighted with Source Code Highlighter.


Эталонный расчет


В качестве эталона будем считать простой цикл for от 2 до limit:

public List<int> Find(int limit)<br> {<br> var result = new List<int>();<br><br> for(int i = 2; i < limit; i++)<br> {<br> if(Prime.IsPrime(i))<br> result.Add(i);<br> }<br><br> return result;<br> }<br><br>* This source code was highlighted with Source Code Highlighter.

Если мы запустим нашу программу, то увидим примерно следующий график загрузки процессора при поиске всех простых чисел до 200 000:

CPU load

Процессор работает только в пол силы. Попробуем это исправить.

Parallel.For


Большое преимущество этой библиотеки в том, что для создания многопоточного код а требуется минимум изменений. В нашем случае метод Parallel.For() принимает три параметра: начало интервала, конец и делегат для выполнения. Заметьте, что модификация не затрагивает тело цикла:

public List<int> Find(int limit)<br> {<br> var result = new List<int>();<br><br> Parallel.For(2, //start from<br> limit, //up to<br> i => //action variable<br> {<br> if(Prime.IsPrime(i))<br> result.Add(i);<br> });<br><br> return result;<br> }<br><br>* This source code was highlighted with Source Code Highlighter.

Изменения минимальны, но давайте посмотрим, на график загрузки процессора:

Full cpu load

Используются оба ядра. И время выполнения сократилось почти в два раза (одна клетка — 5 секунд). Стоит заметить, что List<T> позволяет добавлять элементы в разных потоках. А вот если бы мы попытались пройтись по с списку с помощью foreach, то получили бы ошибку. Вся работа с синхронизацией так и лежит на пользователе.

AsParallel()


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

Enumerable.Range ( 2, limit - 1 ).Where ( Prime.IsPrime ).ToList ( );<br><br>* This source code was highlighted with Source Code Highlighter.

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

Enumerable.Range(2, limit - 1).AsParallel().Where(Prime.IsPrime).ToList();<br><br>* This source code was highlighted with Source Code Highlighter.

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

Parallel.Invoke()


Parallel.Invoke() в основном используется для выполнения разноплановых задач. В нашем варианте мы разобьем все числа из диапазона на 10 частей и выполним расчет в виде параллельных задач. Для этого создадим дополнительный метод помощник, который в качестве аргументов получает верхнюю границу, количество поддиапазонов и номер поддиапазона для расчета:

private static List<int> CheckRange(int n, int limit, int factor)<br> {<br> var local_result = new List<int>();<br> for(int i = n*(limit/factor); i < (n + 1)*(limit/factor); i++)<br> {<br> if(Prime.IsPrime(i))<br> local_result.Add(i);<br> }<br> return local_result;<br> }<br><br>* This source code was highlighted with Source Code Highlighter.

В качестве аргумента Parallel.Invoke() в нашем случае принимает массив делегатов. Новый код будет выглядеть так:

public List<int> Find(int limit)<br> {<br> var result = new List<int>();<br> Parallel.Invoke(() => result.AddRange(CheckRange(0, limit, 10)),<br> () => result.AddRange(CheckRange(1, limit, 10)),<br> () => result.AddRange(CheckRange(2, limit, 10)),<br> () => result.AddRange(CheckRange(3, limit, 10)),<br> () => result.AddRange(CheckRange(4, limit, 10)),<br> () => result.AddRange(CheckRange(5, limit, 10)),<br> () => result.AddRange(CheckRange(6, limit, 10)),<br> () => result.AddRange(CheckRange(7, limit, 10)),<br> () => result.AddRange(CheckRange(8, limit, 10)),<br> () => result.AddRange(CheckRange(9, limit, 10)));<br><br> return result;<br> }<br><br>* This source code was highlighted with Source Code Highlighter.

Мы, конечно, сильно изменили код, но это не типичная ситуация.

Немного статистики


Для наглядности привожу график скорости выполнения:

Execution time

И график загрузки процессора (немного растянул):

CPU load

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

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

Дайте два!


  1. Страница загрузки Parallel Extensions
  2. Исходные коды примера: Look4Prime


И это все?!?


Нет, не все. Вот ссылки для более углубленного изучения:


Теги:Parallel Extensions.netmulticoreоптимизация
Хабы: .NET
+57
5,3k 62
Комментарии 69
Похожие публикации
Рефакторинг кода .NET
7 декабря 202030 200 ₽Luxoft Training
Machine Learning. Professional
26 ноября 202048 000 ₽OTUS
Machine Learning для руководителей
26 ноября 2020БесплатноOTUS
Тренажер product-менеджера
26 ноября 202028 900 ₽SkillFactory
▇▅▄▅▅▄ ▇▄▅