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
{
/// <summary>
/// Determines whether the specified candidate is prime.
/// </summary>
/// <param name="candidate">The candidate.</param>
/// <returns>
/// <c>true</c> if the specified candidate is prime; otherwise, <c>false</c>.
/// </returns>
public static bool IsPrime(int candidate)
{
for(int d = 2; d <= candidate/2; d++)
{
if(candidate%d == 0)
return false;
}
return candidate > 1;
}
}
* This source code was highlighted with Source Code Highlighter.Для удобства создадим интерфейс:
interface IPrimeFinder
{
/// <summary>
/// Finds primes up to the specified limit.
/// </summary>
/// <param name="limit">The limit.</param>
/// <returns>List of primes</returns>
List<int> Find ( int limit );
}
* This source code was highlighted with Source Code Highlighter.Эталонный расчет
В качестве эталона будем считать простой цикл for от 2 до
limit:public List<int> Find(int limit)
{
var result = new List<int>();
for(int i = 2; i < limit; i++)
{
if(Prime.IsPrime(i))
result.Add(i);
}
return result;
}
* This source code was highlighted with Source Code Highlighter.Если мы запустим нашу программу, то увидим примерно следующий график загрузки процессора при поиске всех простых чисел до 200 000:

Процессор работает только в пол силы. Попробуем это исправить.
Parallel.For
Большое преимущество этой библиотеки в том, что для создания многопоточного код а требуется минимум изменений. В нашем случае метод
Parallel.For() принимает три параметра: начало интервала, конец и делегат для выполнения. Заметьте, что модификация не затрагивает тело цикла:public List<int> Find(int limit)
{
var result = new List<int>();
Parallel.For(2, //start from
limit, //up to
i => //action variable
{
if(Prime.IsPrime(i))
result.Add(i);
});
return result;
}
* This source code was highlighted with Source Code Highlighter.Изменения минимальны, но давайте посмотрим, на график загрузки процессора:

Используются оба ядра. И время выполнения сократилось почти в два раза (одна клетка — 5 секунд). Стоит заметить, что
List<T> позволяет добавлять элементы в разных потоках. А вот если бы мы попытались пройтись по с списку с помощью foreach, то получили бы ошибку. Вся работа с синхронизацией так и лежит на пользователе.AsParallel()
Одним из моих самых любимых новшеств в .net стал LINQ. Он позволяет сократить многие конструкции до одной строчки. Вот как будет выглядеть наш пример, записанный с помощью LINQ:
Enumerable.Range ( 2, limit - 1 ).Where ( Prime.IsPrime ).ToList ( );
* This source code was highlighted with Source Code Highlighter.Коротко и ясно. Для того, чтобы сделать этот запрос параллельным нужно дописать всего одно слово
AsParallel():Enumerable.Range(2, limit - 1).AsParallel().Where(Prime.IsPrime).ToList();
* 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)
{
var local_result = new List<int>();
for(int i = n*(limit/factor); i < (n + 1)*(limit/factor); i++)
{
if(Prime.IsPrime(i))
local_result.Add(i);
}
return local_result;
}
* This source code was highlighted with Source Code Highlighter.В качестве аргумента
Parallel.Invoke() в нашем случае принимает массив делегатов. Новый код будет выглядеть так:public List<int> Find(int limit)
{
var result = new List<int>();
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;
}
* This source code was highlighted with Source Code Highlighter.Мы, конечно, сильно изменили код, но это не типичная ситуация.
Немного статистики
Для наглядности привожу график скорости выполнения:

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

На графике загрузки хорошо видны паттерны из трех методов, использующих параллельные вычисления и одного эталонного.
Даже при маленьком количестве вычислений, когда процессоры загружены не полностью, использование PE дает практически двукратный прирост скорости. Существует небольшой оверхеад при первом обращении к библиотеке. Выбор же типа параллельных вычислений на скорость практически не влияет.
Дайте два!
- Страница загрузки Parallel Extensions
- Исходные коды примера: Look4Prime
И это все?!?
Нет, не все. Вот ссылки для более углубленного изучения:
- Подразделение Parallel Computing
- Parallel FX Team Blog
- Parallel FX Library в Википедии
- Running Queries On Multi-Core Processors
_________
Текст подготовлен в ХабраРедакторе



комментарии (69)