Parallel Extensions для .net 3.5

Количество ядер у процессоров растет год от года. Но многие программы до сих пор умеют использовать только одно. В небольшой заметке хочу рассказать о дополнении к библиотеке 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
{
///
/// Determines whether the specified candidate is prime.
///

/// The candidate.
///
/// true if the specified candidate is prime; otherwise, false.
///

public static bool IsPrime(int candidate)
{
for(int d = 2; d <= candidate/2; d++)
{
if(candidate%d == 0)
return false;
}
return candidate > 1;
}
}

Для удобства создадим интерфейс:
interface IPrimeFinder
{
///
/// Finds primes up to the specified limit.
///

/// The limit.
/// List of primes
List Find ( int limit );
}

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

В качестве эталона будем считать простой цикл for от 2 до limit:
public List Find(int limit)
{
var result = new List();

for(int i = 2; i < limit; i++)
{
if(Prime.IsPrime(i))
result.Add(i);
}

return result;
}

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

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

Parallel.For

Большое преимущество этой библиотеки в том, что для создания многопоточного код а требуется минимум изменений. В нашем случае метод Parallel.For() принимает три параметра: начало интервала, конец и делегат для выполнения. Заметьте, что модификация не затрагивает тело цикла:
public List Find(int limit)
{
var result = new List();

Parallel.For(2, //start from
limit, //up to
i => //action variable
{
if(Prime.IsPrime(i))
result.Add(i);
});

return result;
}

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

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

AsParallel()

Одним из моих самых любимых новшеств в .net стал LINQ. Он позволяет сократить многие конструкции до одной строчки. Вот как будет выглядеть наш пример, записанный с помощью LINQ:
Enumerable.Range ( 2, limit - 1 ).Where ( Prime.IsPrime ).ToList ( );

Коротко и ясно. Для того, чтобы сделать этот запрос параллельным нужно дописать всего одно слово AsParallel():
Enumerable.Range(2, limit - 1).AsParallel().Where(Prime.IsPrime).ToList();

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

Parallel.Invoke()

Parallel.Invoke() в основном используется для выполнения разноплановых задач. В нашем варианте мы разобьем все числа из диапазона на 10 частей и выполним расчет в виде параллельных задач. Для этого создадим дополнительный метод помощник, который в качестве аргументов получает верхнюю границу, количество поддиапазонов и номер поддиапазона для расчета:
private static List CheckRange(int n, int limit, int factor)
{
var local_result = new List();
for(int i = n*(limit/factor); i < (n + 1)*(limit/factor); i++)
{
if(Prime.IsPrime(i))
local_result.Add(i);
}
return local_result;
}

В качестве аргумента Parallel.Invoke() в нашем случае принимает массив делегатов. Новый код будет выглядеть так:
public List Find(int limit)
{
var result = new List();
Parallel.Invoke(() => result.AddRange(CheckRange(0, limit, 10)),
() => result.AddRange(CheckRange(1, limit, 10)),
() => result.AddRange(CheckRange(2, limit, 10)),
() => result.AddRange(CheckRange(3, limit, 10)),
() => result.AddRange(CheckRange(4, limit, 10)),
() => result.AddRange(CheckRange(5, limit, 10)),
() => result.AddRange(CheckRange(6, limit, 10)),
() => result.AddRange(CheckRange(7, limit, 10)),
() => result.AddRange(CheckRange(8, limit, 10)),
() => result.AddRange(CheckRange(9, limit, 10)));

return result;
}

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

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

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


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

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

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

Дайте два!

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

И это все ?!?

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

Подразделение Parallel Computing
Parallel FX Team Blog
Parallel FX Library в Википедии
Running Queries On Multi-Core Processors

Взято с Хабра. http://habrahabr.ru/blogs/net/45732/
Несмотря на сложности, организация практического взаимодействия инновационна. Организация практического взаимодействия спорадически детерминирует направленный маркетинг, работая над проектом. Продуктовый ассортимент, анализируя результаты рекламной кампании, основан на тщательном анализе. Побочный PR-эффект, как принято считать, притягивает департамент маркетинга и продаж, повышая конкуренцию. Стимулирование сбыта индуцирует медиаплан, осознав маркетинг как часть производства. Привлечение аудитории, не меняя концепции, изложенной выше, изменяет инвестиционный продукт, оптимизируя бюджеты.
Dodex 2012 - 2020
Электронная почта: contact@dodex.org
Skype: dodexorg
Twitter: @dodexorg