
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:

Процессор работает только в пол силы. Попробуем это исправить.
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.
Изменения минимальны, но давайте посмотрим, на график загрузки процессора:

Используются оба ядра. И время выполнения сократилось почти в два раза (одна клетка — 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.
Мы, конечно, сильно изменили код, но это не типичная ситуация.
Немного статистики
Для наглядности привожу график скорости выполнения:

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

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