вторник, 28 декабря 2010 г.

Введение в паралельные миры .NET 4.0

Перевод статьи Introduction to Parallelism in .NET 4.0 by Johnatan Cardy
В недалеком прошлом, процессоры имели как правило одно ядро, но благодаря тому что из года в год производители увеличивали его частоту, увеличивалась таким образом и общая производительность процессора. Это означало, что производительность многопоточного кода, написанного для одноядерного процессора хоть и несильно но все таки повышалась (одноядерные процессоры как правило могут только эмулировать выполнение нескольких потоков одновременно). В последние несколько лет, частота достигла своего определенного экстремума и теперь увеличение производительности процессоров осуществляется за счет увеличения количества ядер. Так, большинство процессоров сегодня обладают двумя и немного реже четырмя ядрами. Вместе с тем прослеживается тенденция, что и в будуем производительность процессоров будет прирастать в первую очередь за счет увеличения количества физических ядер.
Создание корректных паралельных программ сопряжено с определенными сложностями, возникающих в процессе разработки. К таким сложностям относятся синхронизация общих данных, поточная безопасность кода, исключение ситуаций гонок данных (race conditions) и блокировки данных (deadlocking) и многие другие вопросы. Зачастую решение данных проблем является очень затратным в как в плане времени так и в плане высокой стоимости квалифицированных сотрудников, способных решать подобные задачи. В данной статье я попробую познакомить читателей с новыми особенностями .NET 4.0, которые предназначены для помощи в написании масштабируемого кода, использующего возможности паралелизма, многопоточности и эффективного использования многоядерных процессоров.
Заметка: Термин «многоядерный» описывает процессор, содержащий большее количество ядер, чем количество выполняющихся процессов, а это может быть сотни или даже тысячи ядер. С этой точки зрения – паралелизм в программном обеспечении это ключ; нельзя сказать что система работает эффективно если количество имеющихся ядер больше количества потоков. Если вы считаете процессоры с таким количеством ядер мифическими, подумайте вот о чем – NVIDIA разрабатывает свои паралельные процессоры с учетом этих требований паралелизма. Например NVIDIA GTX280 GPU имеет 240 ядер, Tesla M20(http://www.nvidia.com/object/product_tesla_c1060_us.html) – целых 448. Intel также аннонсировала 50-ядерный процесор по кодовым названием «Knights Corner» (http://www.intel.com/pressroom/archive/releases/20100531comp.htm).
Часто, когда мы используем распаралеливание в наших приложениях, мы пишем код, который адаптирован к конкретному количеству ядер. Например, я хочу освободить мой UI поток посредством выполнения времязатратных вычислений в фоне. Таким образом, я запускаю мои операции вычисления в новом потоке, который беру из пула потоков. Но что если мой процессор имеет 4 ядра? Или 1000? Я же жестко запрограммировал использование только двух потоков. Это главный момент, который бы акцентирован в исследовательском отчете 'The Landscape of Parallel Computing Research: A View from Berkeley(http://www.eecs.berkeley.edu/Pubs/TechRpts/2006/EECS-2006-183.pdf)' университета Калифорнии Беркли в 2006 году. В частности там сказано:
«Главной целью должна быть возможность написать программу которая эффективно выполняется на высоко-распаралеленой вычислительной системе»
и -
«Для того что бы добиться успеха, программные модели не должны зависеть от количества процессоров»
Не беспокойтесь – все эти вопросы аддресуются реализации .NET 4.0, которая призвана изолировать разработчика от низкоуровневых концепций многопоточности. Сейчас мы должны сфокусироваться в первую очередь на том, что выполняется паралельно, а не как. С данным подходом, мы можем обеспечить в будущем гибкость и масшабируемость нашего программного обеспечения при использовании его на многоядерных системах. Px (Parallel Extensions) – широкий термин для для описания механизмов паралелизма в .NET 4.0. Он состоит из Task Parallel Library и PLINQ. Ниже, я вкраце опишу каждый из них, и как бонус представлю вашему вниманию Rx – Reactive Extensions – легкий фреймворк основанный на Px.

Task Parallel Library

(http://www.microsoft.com/downloads/en/details.aspx?FamilyID=86B3D32B-AD26-4BB8-A3AE-C1637026C3EE)

Данная библиотека помимо всего прочего содержит фуникции для распаралеливания циклов for и foreach, и вводит новый тип Task, который может использоватся как альтернатива ThreadPool. Данный тип используется для представления операции, которой может понадобиться некоторое время для ожидания без блокировки своего потока.

  • Parallel.For и Parallel.ForEach: создает делегат Task для реализации тела цикла, которое будет вызываться паралельно.
  • Parallel.Invoke: разрешает паралельные вызовы множества операций.
  • Task.WaitAll: блокирует только главный поток, ожидая, пока завершатся все задачи, вместо того, что бы блокировать каждый поток в отдельности.
  • Task.Factory.ContinueWhenAll: создает задачу (task) которая представляет выполнение определенного числа подзадач.
Предупреждение: Не стоит слишком переусердствовать приведением ваших паралельных алгоритмов к форме использующей паралельные конструкции если ваш код достаточно простой. Это связано с тем, что накладные расходы связанные с синхронизацией (для балансировки нагрузки потоков) и вызовы делегатов, которые осуществляются при выполнении задач, могут только все ухудшить.

PLINQ

(http://msdn.microsoft.com/en-us/library/dd997425.aspx)

PLINQ – это набор расширенных методов (extension methods) которые могут использоваться для паралельного выполнения запросов обычного LINQ. Сюда также входят дополнительные расширения которых нету в последовательном LINQ. Расширенные методы находятся в типе ParallelQuery<T>, который можно получить вызвав предварительно метод AsParallel() объекта IEnumerable. Ниже я представлю несколько расширений, для того что бы вы могли представить что входит в PLINQ:
  • Select, Where, OrderBy, Aggregate, All, и.т.д.: Паралельные версии стандартных операторов.
  • ForAll: Выполняет действие для каждого элемента коллекции ParallelQuery. Паралельно, разумеется.
  • AsOrdered: Сигнализирует, что паралельный запрос должен вести себя как упорядоченный – таким образом, после того как все части паралельной операции завершениы, вы можете быть уверены что все элементы в коллекции результата, отсортированы в заданном порядке.
  • AsUnordered: сигнализирует что запросу ParallelQuery не требуется получать результат в каком то определенном порядке. Эта логика используется по умолчанию.
  • AsSequential: преобразует ParallelQuery в IEnumerable, вынуждая операцию выполнятся последовательно.
  • WithCancellation: позволяет передавать токен отмены, делая возможным прерывание паралельной операции.
  • WithExecutionMode: PLINQ иногда решает что выполнение операции последовательно более эффективно. Однако, если вам лучше известно как нужно осуществлять вычисление, вы можете форсировать это указав подобным образом что необходимо всегда выполнять операцию паралельно.
Предупреджение: Когда осуществляется замещение запросов паралельными версиями, все что вы хотите выполнить паралельно должно быть независимым друг от друга. Будте аккуратны с лямбда-выражениями, не позволяя им допускать побочные эффекты и сделайте их действительно паралельными.

Rx

(http://download.microsoft.com/download/C/5/D/C5D669F9-01DF-4FAF-BBA9-29C096C462DB/Rx%20HOL%20.NET.pdf)

Rx (Reactive Extensionы) похож на LINQ, но только работает как бы наоборот. В то время как LINQ представляет собой множество расширенных методов для IEnumerable, то Rx – это множество расширенных методов для IObservable. Главным моментом здесь является тот факт, что IObservable является математическим двойником IEnumerable – коллекция IEnumerable – это коллекция для извлечений (pull collection), а IObservable – коллекция для размещения (push collection). Эти коллекции и эквиваленты и противоположны друг другу одновременно. Вместо того, что бы запрашивать каждый элемент в отдельности из коллекции, как бы вы поступали в случае с IEnumerable, для IObservable действует другая логика – вы получаете уведомление каждый раз, когда новый элемент помещается в коллекцию и еще одно уведомление когда достигается конец коллекции.
Подождите, но каким образом это имеет отношение к паралелизму о котором мы говорим в данной статье? Что же, у Rx непосредственное отнощение к паралелизму, с его помощью становится возможным комбинировать данные из асинхронных источников в вашем приложении. Эти асинхронные источники могут быть частью различных паралельных вычислений – или если говорить об этом более простыми словами, то они, например, могут передаваться через разные UI события из различных потоков. IObservable будет осуществлять обработку всех сложных моментов, с которыми сопряжена работа с несколькими потоками, а разработчик может сфокусироваться непосредственно только на своей прикладной задачей. Стоит отметить, что Rx зависит от Px, так как глубоко в своих недрах он использует операции Px.
В следующем примере, создается IObservable который ссылается на источник. После это осуществляется вызов Subscribe для данного источника и передача некоторых действий, которые необходимо выполнить для каждого элемента, по достижению конца коллекции и в случае ошибки:
IObservable<int> source = Observable.Empty<int>();
Как только вы создали свой источник, вы должны подписаться на уведомления. Это осуществляется с помощью метода Subscribe:
IDisposable subscription = source.Subscribe(
item => {}, //что-то делаем с элементом коллекции
ex => {}, //обрабатывает исключение
() => {} //что-то делаем при завершении коллекции
);
В примере выше у нас в качестве источника используется пустая коллекция, и поэтому мы просто получим уведомление OnCompletion. Очевидно, что с пустой коллекции толку мало, ниже представлен список ситуаций, где и как можно создать IObservable:
  • Можно создать коллекцию IObservable из какого-то вычисления, выполняемого длительное время, и реализовать код, который асинхронно обрабатывает результат по мере его появления.
  • Можно накапливать данные получаемые посредством ввода пользователя в коллекции IObservable, например – события передвижения мыши можно представить как бесконечную коллекцию элементов которые представляют собой текущие координаты мыши.
  • Любое другое событие .NET может быть использовано для создания IObservable с помощью использования метода Observable.FromEvent.
Доступно большое количество расширенных методов LINQ – Where, Select, Skip – они делают то же что и в случае с обычным LINQ. Ниже представлен упрощенный пример исходного кода с использованием IObservable, который был взят из документации Hands On Labs, которые я рекомендую к прочтению, если вы планируете использовать Rx в своей работе.
//прослушиваем событие TextChanged для textbox
var src = Observable.FromEvent<EventArgs>(textBox, "TextChanged").Select(evt => ((TextBox)evt.Sender).Text) //получаем текст
.Throttle(TimeSpan.FromSeconds(1)) //максимум 1 сообщение в секунду
.DistinctUntilChanged(); //Игнорируем эквивалентные дубликаты

//Вызываем ObserveOn для синхронизации с потоком UI, и записываем текст в метку.
using(src.ObserveOn(label).Subscribe(
txt =>
{
label.Text = txt;
})
{
Application.Run(frm);
}
Оригинальный пример из документации который я рассмотрел выше идет немного дальше – отсылает элементы в хранилище через веб-сервис (у Rx имеется встренная поддержка асинхронной работы с веб-сервисами), получает ответ, вызывает SelectMany для того что бы поставить полученные результаты в соответствие с инициировавшим их запросом и помещением их в ListView. И все это помещается буквально в нескольких строчках, причем никакого спагетти-кода, все чинно и интуитивно понятно.

Выводы

Целью данной статьи является поверхностное знакомство с особенностями реализации паралелизма, которые существуют в .NET 4.0 и я старался изо всех сил не вдаваться в ненужные подробности. Например, из того что я не рассмотреть, стоит обязательно отметить что существует поддержка обработки исключений, возникающих при паралельных вычислениях, определение количества задач (потоков) выполняемых в фоне и управление этими задачами – все эти вещи так и иначе будут использоваться в реальных приложениях. Помимо этого, фреймворк содержит огромное количество другой функциональности, о которой я не упомянул даже вскольз.
Я надеюсь, сейчас вы понимаете потребности и возможности поддержки паралельных вычислений, существующих в .NET. Если какая либо из описаных выше возможностей вас заинтересовала, обратитесь за дополниетльной информацией к источникам, которые представлены гиперссылками в статье. Я это крайне рекомендую, так как чувствую, что в будущем эта информация будет чрезвычайно полезной и обязательной для любого .NET разработчика.
Спасибо, Джонни

 

Комментариев нет: