Краткий обзор Parallel Extensions для .NET Framework

Автор: Topol Суббота, Май 5th, 2012 Нет комментариев

Рубрика: Операционные системы

Сейчас никого не удивишь наличием нескольких ядер в процессоре, будь то рабочая станция, ноутбук, нетбук — неважно. Дальше — больше: «двухголовые» процессоры скоро обоснуются и в мобильных телефонах. Вот почему именно сейчас тема распараллеливания программ перестаёт быть академической и приобретает вполне практический интерес. Разумеется, при создании такого ПО есть ряд подводных камней. Кроме того, не всё и не всегда можно и нужно распараллеливать — но это тема отдельного разговора. Цель этой статьи — сделать экскурс в область инструментов от компании Microsoft для создания распараллеленных управляемых приложений.

Итак, Parallel Extensions, как можно догадаться по названию, это как раз и есть тот самый набор инструментов, призванный повысить эффективность применения новых аппаратных средств. Впервые в общем доступе библиотека появилась в ноябре 2007 года и имела статус CTP. В июне 2008 года появилась новая preview-версия. Обе они использовали .NET  3.5 для своей работы, и поставлялись в виде отдельной сборки. Однако осенью прошлого года с выходом Visual Studio 2010 CTP этот инструментарий переехал в сборку mscorlib и стал использовать .NET 4. С тех пор ничего не поменялось, и новых версий для .NET 3.5 не выпускалось. В течение всего этого времени менялся состав библиотеки, её API. Наиболее актуальная версия не так давно выпущена в составе .NET 4 beta 2. Вы можете скачать Visual Studio 2010 beta 2 и попробовать библиотеку своими руками.

Схематически состав Parallel Extensions можно изобразить следующим образом:

.NET 4 несёт весомое количество изменений. Они коснулись в том числе и принципа работы пула потоков. Его оптимизировали для управления большим числом рабочих потоков, и поменяли реализацию очереди задания для каждого из них. Теперь кроме глобальной очереди задач каждый поток имеет свою локальную очередь. Это позволяет более эффективно управлять задачами. Например, первый поток выполняет очередную задачу, а второй успел опустошить свою очередь. Тогда он может «украсть» задачу из очереди первого потока, тем самым распределив нагрузку. При этом не будет обращения к глобальной очереди задач, что уменьшит нагрузку на неё. Эти действия будут прозрачны для прикладного программиста.

Если внимательно присмотреться к предыдущему абзацу, можно заметить ключевое слово «задача». Оно знаменует один из подходов к упрощению распараллеливания программ. Библиотека Parallel Extensions предлагает сосредоточить внимание на реализуемом бизнес-сценарии. Слово «поток» относится к технической реализации. «Задача» же представляет собой некоторое атомарное действие, выполнение которого позволит получить результат. При этом не рассматривается, кем и где будет выполнена задача. Важнее то, как она реализуется и как соотносится с другими задачами. Такова концепция, положенная в основе Task Parallel Library (кратко TPL) — набора инструментов для создания, управления и выполнения задач. При этом TPL использует механизмы обновленного пула потоков для запуска и завершения задач. Фактически, задача представляет собой некую асинхронную операцию в контексте нашей программы. Конечно, в .NET 2.0 были QueueUserWorkItem(), BackgroundWorker и др. средства. С их помощью можно было достичь нужного функционала, что все мы с вами успешно делали. Но TPL предоставляет уже готовый, до некоторой степени универсальный подход. Это уменьшает затраты на распараллеливание, что в конечном счёте делает проект дешевле по экономическим и временным показателям.

На основе Task Parallel Library и концепции «задач» работают прикладные средства по распараллеливанию. Среди них — PLINQ, т.е. Parallel LINQ. Эта часть библиотеки Parallel Extensions, позволяющая выполнять LINQ запросы одновременно в нескольких потоках. Изюминка её заключается в очень простом способе интеграции с уже имеющимся кодом. Не придется снова переписывать кучу запросов на новый лад. Можно просто вызвать метод-расширение AsParallel(), и теперь LINQ запрос будет выполнен параллельно:

Последовательная версия Параллельная версия
from i in collection from i in collection.AsParallel()
where IsValid(i) where IsValid(i)
select i; select i;

Разумеется, возникает много интересных моментов — например, в нашем примере никто не знает, в каком порядке элементы будут обработаны при параллельном исполнении. Есть свои особенности, связанные с обработкой исключений. Опять же, не всякий LINQ-запрос есть смысл распараллеливать. Как всегда, нужно правильно оценивать ситуацию и действовать по обстоятельствам. Но наличие такой легкой возможности распараллелить процесс выполнения, безусловно, радует. Ещё одним преимуществом PLINQ является масштабируемость. Поясню: запрос будет выполнен настолько быстро, насколько это можно в данной конкретной среде. Наша программа без перекомпиляции и без внесения каких-либо изменений будет работать с эффективностью 1х на одноядерной машине, 1,8х-1,9х на двухядерной, 3,7х-3,8х на четырехядерной и т.д. (числа приведены приблизительные, для сравнения; реальная разница, конечно, зависит от задачи и способа её реализации). Т.е. чем более мощное оборудование будет использовано, тем быстрее выполнится запрос. PLINQ физически является набором методов-расширений, работающих с System.Linq.ParallelQuery<T>.

Кроме того, Parallel Extensions содержит несколько конструкций, облегчающих создание распараллеленных циклов и запуск множества делегатов одновременно. Это набор статических методов класса Parallel. Их много, но большинство являются перегруженными версиями следующих трёх:

  • Parallel.For() — метод позволяет выполнить делегат параллельно в несколько потоков заданное количество итераций;
  • Parallel.ForEach() — параллельно выполняет делегат над элементами коллекции IEnumerable<TSource>;
  • Parallel.Invoke() — принимает набор делегатов Action и обеспечивает их одновременное выполнение

Разные версии этих методов содержат дополнительные параметры, с помощью которых можно указать, например, степень параллелизма, маркер отмены и т.д. Давайте рассмотрим несколько простых примеров. Предположим, есть следующий цикл foreach:

Код:
IEnumerable<string> collection =
new[] { «The», «quick», «brown», «fox», «jumps», «over», «the», «lazy», «dog»};

foreach (var word in collection)
{
ProcessString(word);
}

Чтобы перевести этот цикл на «параллельные рельсы», достаточно заменить его вызовом:

Код:
Parallel.ForEach(collection, (word) => { ProcessString(word); });

Теперь обработка слов будет выполнена параллельно. Каждый вызов Parallel.For() и Parallel.ForEach() генерирует определённое количество задач, в зависимости от текущего окружения, после чего передаёт их для выполнения рабочим потокам. Т.о., цикл будет выполнен настолько быстро, насколько это возможно.

А вот так можно ограничить степень параллелизма:

Код:
ParallelOptions options = new ParallelOptions();
options.MaxDegreeOfParallelism = 4;

Parallel.For(0, 100, options, (i) => { ProcessItem(i); });

Как видите, используется перегруженная версия Parallel.For(), принимающая настройки (ParallelOptions).

В довесок пример обхода дерева следующего вида:

Код:
class Tree<T>
{
public T Data;
public Tree<T> Left, Right;
// …
}

Последовательно это можно сделать следующим образом:

Код:
static void WalkTree<T>(Tree<T> tree, Action<T> func)
{
if (tree == null) return;
WalkTree(tree.Left, func);
WalkTree(tree.Right, func);
func(tree.Data);
}

Однако, если вычисления не зависят от порядка обработки узлов, то можно легко распараллелить процесс:

Код:
static void WalkTree<T>(Tree<T> tree, Action<T> func)
{
if (tree == null) return;
Parallel.Invoke(
() => WalkTree(tree.Left, func),
() => WalkTree(tree.Right, func),
() => func(tree.Data));
}

Указываем набор делегатов, а именно проход по левому и правому потомкам текущего узла, и обработку текущего в качестве параметров Parallel.Invoke() и радуемся возросшей производительности. Надо отметить, что Parallel.Invoke() также поддерживает указание ParallelOptions, для этого есть соответствующий перегруженный метод.

В довесок к этому Parallel Extensions предоставляет весьма удобные хранилища данных с возможностью конкурентного доступа и набор примитивов синхронизации. Это выглядит совершенно логично: выполняя действия в нескольких потоках, неплохо было бы иметь где-либо результат работы. У меня есть 2 большие статьи, в которых проводится обзор этих структур данных, предлагаю всем заинтересовавшимся их прочитать: часть 1 и часть 2. Здесь я не стану подробно на них останавливаться.

Итак, подведем итог. То, что компания Microsoft уделила большое внимание вопросам параллельного выполнения кода, весьма радует. Инструментарий, на мой взгляд, получился достаточно удобным в использовании и довольно простым. Вообще, малый входной порог — один из козырей Parallel Extensions. Концепция и API прозрачны и понятны. Мы получили (я говорю именно «получили», поскольку уже сейчас вышла GoLive лицензия на VS 2010 beta 2, позволяющая строить коммерческие приложения на её основе) удобную библиотеку, которая позволит минимизировать расходы на распараллеливание.

Напоследок несколько полезных ссылок:
1. Блог команды Parallel Extensions: http://blogs.msdn.com/pfxteam/
2. Форум MSDN, посвященный Parallel Extensions: http://social.msdn.microsoft.com/Forums/en-US/parallelextensions/threads/
3. Блог Daniel Moth, DPE из MS: http://www.danielmoth.com/Blog/. Он частенько публикует материалы, касающиеся средств распараллеливания и отладки.

Источник: thevista.ru

Оставить комментарий

Чтобы оставлять комментарии Вы должны быть авторизованы.

Похожие посты