Параллельное программирование


Содержание

Основы программирования параллельных вычислений

Основными особенностями программирования параллельных вычислений являются: более высокая производительность программ и применение специальных приемов программирования, с которыми связаны более высокая трудоемкость и проблемы с переносимостью программ. В отличие от последовательного программирования (на распространенных языках Бейсик, Паскаль, Си, Фортран и др.) при этом возникают дополнительные проблемы: управление работой множества процессоров, организация межпроцессорных пересылок данных и т.д. Наиболее распространенные подходы к распараллеливанию вычислений и обработки данных основаны на моделях параллелизма данных и параллелизма задач.

Модель параллелизма данных предполагает применение одной операции сразу к нескольким элементам массива данных. Векторизация или распараллеливание чаще всего выполняется уже во время трансляции. Основные особенности модели:

· обработкой данных управляет одна программа;

· пространство имен является глобальным, т.е. переменные и массивы размещаются в одной памяти, а детали структуры данных, доступа к памяти и межпроцессорного обмена данными от программиста скрыты;

· слабая синхронизация вычислений на параллельных процессорах – выполнение команд на разных процессорах происходит, как правило, независимо и только иногда производится согласование (синхронизация) выполнения циклов или других элементов программы.

· параллельные операции над элементами массива выполняются одновременно на всех доступных данной программе процессорах.

Упрощению программирования способствует использование базовых операций:

· операций управления данными;

· операций над массивами в целом и их фрагментами;

· операций приведения (вычисления для массива одного числа, например, суммы его элементов);

· операций сканирования, например префиксная операция суммирования, при которой сумма N последовательных элементов одного массива присваивается N-му элементу другого массива;

· операций, связанные с пересылкой данных, например, между массивами разной формы, размерности и протяженности по каждому измерению.

Для составления программ в модели параллелизма данных используются специализированные языки – CM FORTRAN, C*, FORTRAN+, MPP FORTRAN, Vienna FORTRAN, а также HPF (High Performance FORTRAN – высокопроизводительный фортран), основанный на языке программирования FORTRAN-90 с его удобными операциями над массивами. Реализация модели параллелизма данных выполняется на уровне трансляции программы на машинный язык с помощью препроцессоров, использующих библиотеки с реализациями параллельных алгоритмов, предтрансляторов для предварительного анализа и оптимизации и распараллеливающих трансляторов, которые выявляют параллелизм в исходном коде программы и выполняют его преобразование в параллельные конструкции.

Модель параллелизма задач предусматривает разбиение вычислительной задачи на несколько относительно самостоятельных задач, каждая из которых выполняется на своем процессоре (или компьютере при распределенных вычислениях). Компьютер при этом представляет собой MIMD-машину. Для каждой подзадачи пишется своя собственная программа на обычном языке программирования (обычно на FORTRAN-е или на C). Обмен подзадач результатами работы и получение данных осуществляется вызовом процедур специализированной библиотеки. Для обеспечения эффективного совместного выполнения различных подзадач требуются дополнительные усилия. Дополнительные проблемы, возникающие в методе параллелизма задач:

· повышенная трудоемкость разработки и отладки программ;

· необходимость обеспечения равномерной и сбалансированной загрузки всех процессоров;

· необходимость минимизации обмена данными между задачами для уменьшения времени передачи;

· повышенная опасность тупиковых ситуаций.

Однако положительными сторонами являются большая гибкость и свобода и, как следствие, возможность достижения максимального быстродействия на основе эффективного использования ресурсов. Примерами специализированных библиотек являются распространяемые свободно в исходных кодах библиотеки MPI (Message Passing Interface – Интерфейс Передачи Сообщений) и PVM (Parallel Virtual Machines). Реализации спецификации MPI представляют собой библиотеки подпрограмм, которые могут использоваться в программах на языках C/C++ и FORTRAN. Пример простейшей MPI-программы на языке С:

int main(int argc, char *argv[])

int myid, numprocs;

fprintf(stdout,”Process %d of %d \n”, myid, numprocs);

В программе используются подпрограммы MPI_Comm_size и MPI_Comm_rank. Первая возвращает количество процессоров, входящих в область взаимодействия, коммуникатор со стандартным именем MPI_COMM_WORLD, который указан в качестве ее первого аргумента, вторая подпрограмма определяет ранг процесса.

Средой выполнения параллельных программ являются операционные системы с поддержкой мультипроцессирования, многозадачности и многопоточности, например UNIX, LINUX, Microsoft Windows NT/2000/2003 Server.

Система разработки и выполнения параллельных программ PVM позволяет объединить разнородный набор компьютеров, связанных сетью в общий вычислительный ресурс, называемый Параллельной Виртуальной Машиной. Компьютеры могут быть многопроцессорными машинами любого типа: суперкомпьютерами, рабочими станциями и т.д., объединенными в сети любого вида (Ethernet, ATM и др. Поддерживаются языки C/C++ и FORTRAN и ряд других. Виртуальная машина допускает динамическое изменение конфигурации. Масштабируемость виртуальной машины позволяет включать в ее состав сотни хостов и выполнять в ее среде тысячи процессов. Система PVM может быть реализована в кластерах рабочих станций, имеющих относительно низкую стоимость – для создания кластера можно использовать даже сравнительно недорогие персональные компьютеры. Система PVM состоит из двух основных компонентов:

демона (процесса) pvmd3 (или pvdm), который запускается на всех компьютерах, входящих в состав виртуальной машины; Демоном в ОС UNIX называют процессы, управляющие различными службами, в данном случае выполнением PVM-программ.

· набора библиотек, реализующих операции параллельных вычислений и обмена данными, которые используются в параллельных программах на языках С и С++, а также через функции интерфейса и в программах на языке FORTRAN.

Для работы с виртуальной машиной в интерактивном режиме предназначена программа PVM –консоль.

Современное состояние программирования параллельных вычислений рассмотрено в [65].

Компания NEC предложила новую технологию создания многоядерных процессоров, позволяющую повысить эффективность параллельного выполнения приложений с неадаптированным программным кодом. В ее основе лежит возможность принудительного использования шаблонов разделения выполнения приложений, что ускоряет их работу. Это позволяет избавиться от трудоемкой предварительной отладки программ вручную. [66]

Не нашли то, что искали? Воспользуйтесь поиском:

Параллельное программирование

Материал из ПИЭ.Wiki

Параллельная программа содержит несколько процессов, работающих совместно над выполнением некоторой задачи. Каждый процесс — это последовательная программа, а точнее — последовательность операторов, выполняемых один за другим. Последовательная программа имеет один поток управления, а параллельная — несколько.

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

При любом виде взаимодействия процессам необходима взаимная синхронизация. Существуют два основных вида синхронизации — взаимное исключение и условная синхронизация. Взаимное исключение обеспечивает, чтобы критические секции операторов не выполнялись одновременно. Условная синхронизация задерживает процесс до тех пор, пока не выполнится определенное условие. Например, взаимодействие процессов производителя и потребителя часто обеспечивается с помощью буфера в разделяемой памяти. Производитель записывает в буфер, потребитель читает из него. Чтобы предотвратить одновременное использование буфера и производителем, и потребителем (тогда может быть считано не полностью записанное сообщение), используется взаимное исключение. Условная синхронизация используется для проверки, было ли считано потребителем последнее записанное в буфер сообщение.

История развития

Как и другие прикладные области компьютерных наук, параллельное программирование прошло несколько стадий. Оно возникло благодаря новым возможностям, предоставленным развитием аппаратного обеспечения, и развилось в соответствии с технологическими изменениями. Через некоторое время специализированные методы были объединены в набор основных принципов и общих методов программирования.


Параллельное программирование возникло в 1960-е годы в сфере операционных систем. Причиной стало изобретение аппаратных модулей, названных каналами, или контроллерами устройств. Они работают независимо от управляющего процессора и позволяют выполнять операции ввода-вывода параллельно с инструкциями центрального процессора. Канал взаимодействует с процессором с помощью прерывания — аппаратного сигнала, который говорит: «Останови свою работу и начни выполнять другую последовательность инструкций».

Результатом появления каналов стала проблема программирования (настоящая интеллектуальная проблема) — теперь части программы могли быть выполнены в непредсказуемом порядке. Следовательно, пока одна часть программы обновляет значение некоторой переменной, может возникнуть прерывание, приводящее к выполнению другой части программы, которая тоже попытается изменить значение этой переменной. Это специфическая проблема (задача критической секции).

Вскоре после изобретения каналов началась разработка многопроцессорных машин, хотя в течение двух десятилетий они были слишком дороги для широкого использования. Однако сейчас все крупные машины являются многопроцессорными, а самые большие имеют сотни процессоров и часто называются машинами с массовым параллелизмом (massively parallel processors).

Многопроцессорные машины позволяют разным прикладным программам выполняться одновременно на разных процессорах. Они также ускоряют выполнение приложения, если оно написано (или переписано) для многопроцессорной машины. Но как синхронизировать работу параллельных процессов? Как использовать многопроцессорные системы для ускорения выполнения программ?

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

Введение в параллельное программирование

Определение, назначение параллельного программирования

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

  1. Последовательное программирование с дальнейшим автоматическим распараллеливанием.
  2. Непосредственное формирование потоков параллельного управления, с учетом особенностей архитектур параллельных вычислительных систем или операционных систем.
  3. Описание параллелизма без использования явного управления обеспечивается заданием только информационных связей. Предполагается, что программа будет выполняться на вычислительных системах с бесконечными ресурсами, операторы будут запускаться немедленно по готовности их исходных данных.

Каждый из перечисленных подходов обладает своими достоинствами и недостатками параллельное программирование .

Параллельные вычисления — способ организации компьютерных вычислений, при котором программы разрабатываются, как набор взаимодействующих вычислительных процессов, работающих асинхронно и при этом одновременно.

Параллельное программирование — это техника программирования, которая использует преимущества многоядерных или многопроцессорных компьютеров и является подмножеством более широкого понятия многопоточности ( multithreading ).

Многоядерные вычисления

Использование параллельного программирования становится наиболее необходимым, поскольку позволяет максимально эффективно использовать возможности многоядерных процессоров и многопроцессорных систем. По ряду причин, включая повышение потребления энергии и ограничения пропускной способности памяти, увеличивать тактовую частоту современных процессоров стало невозможно. Вместо этого производители процессоров стали увеличивать их производительность за счет размещения в одном чипе нескольких вычислительных ядер, не меняя или даже снижая тактовую частоту. Поэтому для увеличения скорости работы приложений теперь следует по -новому подходить к организации кода, а именно — оптимизировать программы под многоядерные системы.

Множественные потоки команд/данных (Классификация М.Флинна)

Самой ранней и наиболее известной является классификация архитектур вычислительных систем, предложенная в 1966 году М.Флинном. Классификация базируется на понятии потока, под которым понимается последовательность элементов, команд или данных, обрабатываемая процессором. На основе числа потоков команд и потоков данных Флинн выделяет четыре класса архитектур: SISD , MISD , SIMD , MIMD . Описание классов приведено в Табл. 1.1.

Таблица 1.1. Описание классов архитектур
Название класса Описание класса
SISD (single instructiоn streаm / single dаtа streаm) или ОКОД (Одиночный поток Команд, Одиночный поток Данных) Одиночный поток команд и одиночный поток данных (исполнение одним процессором одного потока команд, обрабатывающего данные, хранящиеся в одной памяти). К этому классу относятся, классические последовательные машины, или иначе, машины фон-неймановского типа (PDP-11 или VАX 11/780).
SIMD (single instructiоn streаm / multiple dаtа streаm) или ОКМД (одиночный поток команд, множественный поток данных) Одиночный поток команд и множественный поток данных. В архитектурах подобного рода сохраняется один поток команд, включающий, в отличие от предыдущего класса (SISD), векторные команды, что позволяет выполнять одну арифметическую операцию сразу над многими данными — элементами вектора.
MISD (multiple instructiоn streаm / single dаtа streаm) или МКОД (Множественный поток Команд, Одиночный поток Данных) Множественный поток команд и одиночный поток данных. Определение подразумевает наличие в архитектуре многих процессоров, обрабатывающих один и тот же поток данных.
MIMD (multiple instructiоn streаm / multiple dаtа streаm) или МКМД (Множественный поток Команд, Множественный поток Данных) Множественный поток команд и множественный поток данных. Этот класс предполагает, что в вычислительной системе есть несколько устройств обработки команд, объединенных в единый комплекс и работающих каждое со своим потоком команд и данных.

На основании Табл. 1.1 можно проранжировать архитектуры на однопоточность/многопоточность (Табл. 1.2).

Таблица 1.2. Ранжирование архитектур по обработке потоков
Одиночный поток команд (Single Instructiоn) Множество потоков команд (Multiple Instructiоn)
Одиночный поток данных (Single Dаtа) SISD (ОКОД) MISD (МКОД)
Множество потоков данных (Multiple Dаtа) SIMD (ОКМД) MIMD (МКМД)

Ускорение (Speedup)

Ускорением параллельного алгоритма называется отношение :

где — время вычисления задачи на n процессорах, — время выполнения однопоточной программы

, если параллельная версия алгоритма эффективна.

T(1)» style=»display: inline; «>, если накладные расходы (издержки) реализации параллельной версии алгоритма чрезмерно велики.

С ускорением связана эффективность параллельного алгоритма. Эффективностью параллельного алгоритма называется величина:

По определению, . Теоретически должно быть и . Если алгоритм достигает максимального ускорения , то . На практике эффективность убывает при увеличении числа процессоров.

Если же результат получается 1″ style=»display: inline; «> (суперлинейное ускорение). Эта аномалия вызвана, чаще всего, двумя причинами:

  • В качестве последовательного алгоритма был применён не самый быстрый алгоритм из существующих.
  • С увеличением количества вычислителей растёт суммарный объём их оперативной и кэш памяти. Поэтому всё большая часть данных задачи умещается в оперативной памяти и не требует подкачки с диска, или (чаще всего) умещается в кэше.

Закон Амдала

Закон Амдала (1967 год), описывает максимальный теоретический выигрыш в производительности параллельного решения по отношению к лучшему последовательному решению. Закон Амдала описывается следующей математической формулой:

где — во сколько раз можно ускорить вычисления (ускорение), n — количество процессоров (ядер), — доля последовательно вычисляемого кода ( ).

Закон Амдаля, несмотря на то, что он не учитывает многих факторов, накладывает ограничения на максимально достижимую эффективность параллельного алгоритма.


Предположим, например, что , то есть две трети операций в алгоритме могут выполняться параллельно, а треть — нет. Тогда ускоре-ние . Таким образом, независимо от количества процессоров (ядер) и даже при игнорировании всех затрат на подготовку данных нельзя ускорить решение задачи более, чем в три раза.

Закон Густафсона-Барсиса

Закон Густафсона — Барсиса (1988г) оценивает максимально допустимое ускорение выполнения параллельной программы, в зависимости от количества одновременно выполняемых потоков вычислений и доли последовательных расчётов. Формула Густафсона — Барсиса выглядит следующим образом:

Где — доля последовательных расчётов в программе, — количество процессоров.

Густафсон заметил, что, работая на многопроцессорных системах, пользователи склонны к изменению тактики решения задачи. Теперь снижение общего времени исполнения программы уступает объёму решаемой задачи. Такое изменение цели обусловливает переход от закона Амдала к закону Густафсона. К примеру, на 100 процессорах программа выполняется 20 минут. При переходе на систему с 1000 процессорами можно достичь времени исполнения порядка двух минут. Однако для получения большей точности решения имеет смысл увеличить объём решаемой задачи, т.е. при сохранении общего времени исполнения пользователи стремятся получить более точный результат. Увеличение объёма решаемой задачи приводит к увеличению доли параллельной части, так как последовательная часть ( ввод/вывод , менеджмент потоков, точки синхронизации и т.п.) не изменяется.

ПАРАЛЛЕ́ЛЬНОЕ ПРОГРАММИ́РОВАНИЕ

В книжной версии

Илон Маск рекомендует:  Что такое код msql_num_fields

Том 25. Москва, 2014, стр. 298

Скопировать библиографическую ссылку:

ПАРАЛЛЕ́ЛЬНОЕ ПРОГРАММИ́РОВАНИЕ, раз­ра­бот­ка про­грамм­но­го обес­пе­че­ния, ко­то­рое вы­пол­ня­ет зна­чит. часть вы­чис­ле­ний од­но­вре­мен­но (па­рал­лель­но). Це­ли П. п.: ре­ше­ние боль­ших за­дач с объ­ё­мом дан­ных, пре­вос­хо­дя­щим воз­мож­но­сти од­но­про­цес­сор­ной вы­чис­лит. сис­те­мы; уве­ли­че­ние эф­фек­тив­но­сти про­грамм за счёт па­рал­лель­но­го вы­пол­не­ния как мож­но боль­ше­го чис­ла опе­ра­ций. Воз­мож­ность П. п. обу­слов­ле­на тем, что боль­шие за­да­чи час­то воз­мож­но раз­де­лить на неск. мень­ших под­за­дач, ко­то­рые мо­гут вы­пол­нять­ся од­но­вре­мен­но. В за­ви­си­мо­сти от то­го, как час­то под­за­да­чи долж­ны син­хро­ни­зи­ро­вать­ся или взаи­мо­дей­ст­во­вать ме­ж­ду со­бой, раз­ли­ча­ют мел­ко­зер­ни­стый (fine-grained) и круп­но­зер­ни­стый (coarse-grained) па­рал­ле­лизм про­грамм­ных при­ло­же­ний. В пер­вом слу­чае под­за­да­чи взаи­мо­дей­ст­ву­ют очень час­то (мно­го раз в се­кун­ду), во вто­ром – зна­чи­тель­но ре­же (в иде­аль­ном слу­чае под­за­да­чи не взаи­мо­дей­ст­вуют во­все ли­бо взаи­мо­дей­ст­вие про­исхо­дит край­не ред­ко, напр. в на­ча­ле и в кон­це ра­бо­ты).

Параллельное программирование и библиотека TPL

В эпоху многоядерных машин, которые позволяют параллельно выполнять сразу несколько процессов, стандартных средств работы с потоками в .NET уже оказалось недостаточно. Поэтому во фреймворк .NET была добавлена библиотека параллельных задач TPL (Task Parallel Library), основной функционал которой располагается в пространстве имен System.Threading.Tasks . Данная библиотека позволяет распараллелить задачи и выполнять их сразу на нескольких процессорах, если на целевом компьютере имеется несколько ядер. Кроме того, упрощается сама работа по созданию новых потоков. Поэтому начиная с .NET 4.0. рекомендуется использовать именно TPL и ее классы для создания многопоточных приложений, хотя стандартные средства и класс Thread по-прежнему находят широкое применение.

Задачи и класс Task

В основе библиотеки TPL лежит концепция задач, каждая из которых описывает отдельную продолжительную операцию. В библиотеке классов .NET задача представлена специальным классом — классом Task , который находится в пространстве имен System.Threading.Tasks . Данный класс описывает отдельную задачу, которая запускается асинхронно в одном из потоков из пула потоков. Хотя ее также можно запускать синхронно в текущем потоке.

Для определения и запуска задачи можно использовать различные способы. Первый способ создание объекта Task и вызов у него метода Start:

В качестве параметра объект Task принимает делегат Action, то есть мы можем передать любое действие, которое соответствует данному делегату, например, лямбда-выражение, как в данном случае, или ссылку на какой-либо метод. То есть в данном случае при выполнении задачи на консоль будет выводиться строка «Hello Task!».

А метод Start() собственно запускает задачу.

Второй способ заключается в использовании статического метода Task.Factory.StartNew() . Этот метод также в качестве параметра принимает делегат Action, который указывает, какое действие будет выполняться. При этом этот метод сразу же запускает задачу:

В качестве результата метод возвращает запущенную задачу.

Третий способ определения и запуска задач представляет использование статического метода Task.Run() :

Метод Task.Run() также в качестве параметра может принимать делегат Action — выполняемое действие и возвращает объект Task.

Определим небольшую программу, где используем все эти способы:

Ожидание задачи

Важно понимать, что задачи не выполняются последовательно. Первая запущенная задача может завершенить свое выполнение после последней задачи.

Или рассмотрим еще один пример:

Класс Task в качестве параметра принимает метод Display, который соответствует делегату Action. Далее чтобы запустить задачу, вызываем метод Start: task.Start() , и после этого метод Display начнет выполняться во вторичном потоке. В конце метода Main выводит некоторый маркер-строку, что метод Main завершился.

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

То есть мы видим, что даже когда основной код в методе Main уже отработал, запущенная ранее задача еще не завершилась.

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

Свойства класса Task

Класс Task имеет ряд свойств, с помощью которых мы можем получить информацию об объекте. Некоторые из них:

AsyncState : возвращает объект состояния задачи

CurrentId : возвращает идентификатор текущей задачи

Exception : возвращает объект исключения, возникшего при выполнении задачи

Параллельное программирование в .NET Parallel Programming in .NET

Многие персональные компьютеры и рабочие станции имеют несколько ядер ЦП, которые позволяют одновременно выполнять несколько потоков. Many personal computers and workstations have multiple CPU cores that enable multiple threads to be executed simultaneously. Чтобы воспользоваться преимуществами оборудования, можно параллелизовать код для распределения работы между несколькими процессорами. To take advantage of the hardware, you can parallelize your code to distribute work across multiple processors.

В прошлом распараллеливание требовало управления потоками и взаимоблокировками на низком уровне. In the past, parallelization required low-level manipulation of threads and locks. Visual Studio и .NET Framework обеспечивают расширенную поддержку параллельного программирования, предоставляя среду выполнения, типы библиотек классов и средства диагностики. Visual Studio and the .NET Framework enhance support for parallel programming by providing a runtime, class library types, and diagnostic tools. Эти возможности, которые впервые появились в .NET Framework 4, упрощают параллельную разработку. These features, which were introduced with the .NET Framework 4, simplify parallel development. Это позволяет разработчикам писать эффективный, детализированный и масштабируемый параллельный код с помощью естественных выразительных средств без необходимости непосредственной работы с потоками или пулом потоков. You can write efficient, fine-grained, and scalable parallel code in a natural idiom without having to work directly with threads or the thread pool.

На рисунке ниже представлен общий обзор архитектуры параллельного программирования на платформе .NET Framework. The following illustration provides a high-level overview of the parallel programming architecture in the .NET Framework:


Прошлое, настоящее и будущее распараллеливания .NET-приложений

Стефен Тауб

Продукты и технологии:

Microsoft .NET Framework, TPL, Async, PLINQ

В статье рассматриваются:

  • ограничения ранних версий .NET Framework в создании параллельных приложений;
  • поддержка распараллеливания в .NET Framework 4;
  • будущее параллелизма и параллельной обработки в .NET Framework.

Прошлое

Исторически сложилось так, что для создания «отзывчивых» клиентских приложений, масштабируемых серверов и для распараллеливания алгоритмов разработчики напрямую манипулировали потоками. Но это же вело к взаимоблокировкам (deadlocks), активным блокировкам (livelocks), очередям на блокировках (lock convoys), «топтанию потоков на месте» (two-step dances), конкуренции за блокировки (race conditions), превышению лимита (oversubscription) и уйме других нежелательных проблем в приложениях. С самого начала Microsoft .NET Framework предоставляла мириады низкоуровневых средств для создания параллельных приложений, в том числе целое пространство имен, специально выделенное для этой области: System.Threading. При наличии примерно 50 типов в этом пространстве имен в базовых сборках .NET Framework 3.5 (включая такие типы, как Thread, ThreadPool, Timer, Monitor, ManualResetEvent, ReaderWriterLock и Interlocked) никто не должен был бы винить .NET Framework в легковесном отношении к поддержке потоков. И тем не менее я обвиняю предыдущие версии .NET Framework в таком отношении к реальной поддержке разработчиков, которым нужно было создавать масштабируемые приложения с высокой степенью распараллеливания. С радостью констатирую, что эта проблема устранена в .NET Framework 4 и что в будущих версиях .NET Framework будет внесено много усовершенствований в этой области.

Некоторые могут усомниться в ценности богатой подсистемы в управляемом языке для написания параллельного кода. В конце концов, параллелизм и параллельная обработка сводятся к вопросам производительности, а разработчики, заинтересованные в максимальном быстродействии, должны искать его в неуправляемых языках, которые обеспечивают полный доступ к «железу» и контроль над каждым битом, позволяют манипулировать кеш-линиями и выполнять interlocked-операции…, правильно? Если бы дело и впрямь обстояло таким образом, я бы испугался за состояние нашей индустрии. Существуют управляемые языки вроде C#, Visual Basic и F#, которые предоставляют всем разработчикам — и простым смертным, и супергероям — безопасную, производительную среду для быстрого написания эффективного кода. Разработчикам даются тысячи и тысячи заранее скомпилированных библиотечных классов наряду с языками, напичканными всеми современными сервисами и позволяющими достигать впечатляющих показателей производительности. Все это я говорю, чтобы подчеркнуть, что управляемые языки и связанные с ними инфраструктуры имеют глубокую поддержку для создания высокопроизводительных параллельных приложений, благодаря которой и волки сыты, и овцы целы — даже на современном аппаратном обеспечении.

Я всегда считал, что шаблоны — хороший способ чему-то научиться, поэтому для данной тематики будет тем более правильно, если мы начнем наше исследование с рассмотрения какого-то шаблона. И для «сбивающего с толку», и для «восхитительно» параллельного шаблона одна из наиболее востребованных конструкций разветвления-соединения (fork-join) — параллельный цикл, который предназначен для обработки каждой независимой итерации в цикле параллельно. Очень поучительно посмотреть, как такая обработка могла бы выполняться с использованием ранее упомянутых низкоуровневых примитивов, и для этого мы подробно обсудим базовую реализацию наивного параллельного цикла, реализованного на C#. Возьмем типичный цикл for:

Мы можем напрямую использовать потоки, чтобы добиться распараллеливания этого цикла, как показано на рис. 1.

Рис. 1. Распараллеливание цикла for

Конечно, с этим подходом к распараллеливанию возникает множество проблем. Мы крутим на блокировках новые потоки, выделенные циклу, что не только добавляет издержки (особенно если тело цикла выполняет тривиальную работу), но и может значительно превысить лимиты в процессе, выполняющем параллельно другую работу. Мы используем статическое распределение работы между потоками, способное привести к значительному перекосу нагрузки, если она неравномерно распределена по итерациям (не говоря уж о том, что при количестве итераций, не кратном числу потоков последний из них перегружается). Однако хуже всего, что разработчик вынужден писать такой код. Каждый алгоритм, который мы пытаемся распараллелить, требует аналогичного кода — в лучшем случае весьма хрупкого.

Проблема, проиллюстрированная предыдущим кодом, усугубляется, когда мы осознаем, что параллельные циклы — лишь один из множества шаблонов, существующих в параллельных программах. Заставлять разработчиков выражать все такие параллельные шаблоны на этом низком уровне кодирования — плохая модель программирования, не сулящая успеха широким массам разработчиков, которым нужно использовать возможности аппаратного обеспечения с высокой степенью параллелизма.

Настоящее

Переходим к .NET Framework 4. Этот выпуск .NET Framework был дополнен множеством средств, которые значительно облегчают выражение параллелизма в приложениях и обеспечивают его эффективность. Эти средства выходят далеко за рамки параллельных циклов, но мы, тем не менее, начнем с них.

Пространство имен System.Threading в .NET Framework 4 было расширено новым подпространством имен System.Threading.Tasks. Оно включает новый тип — Parallel, который предоставляет уйму статических методов для реализации параллельных циклов и структурированных шаблонов fork-join. В качестве примера его применения рассмотрим предыдущий цикл for:

С помощью класса Parallel этот цикл можно распараллелить так:

В этом примере разработчик по-прежнему отвечает за то, чтобы каждая итерация цикла была независимой, но в остальном конструкция Parallel.For берет на себя все аспекты распараллеливания этого цикла. Она обрабатывает динамическое распределение входного диапазона между всеми нижележащими потоками, участвующими в этом вычислении, в то же время сводя издержки разделения примерно к тому уровню, что и в реализациях со статическим распределением. Кроме того, данная конструкция динамически увеличивает и уменьшает количество потоков, чтобы подобрать оптимальное их число для текущей рабочей нагрузки (которое не всегда совпадает с количеством аппаратных потоков). Parallel.For предоставляет средства обработки исключений, отсутствующие в моей наивной реализации, показанной ранее, и т. д. Что важнее всего, Parallel.For избавляет разработчика от того, чтобы рассматривать параллелизм на низком уровне абстракции потоков в ОС, и от необходимости каждый раз писать код для весьма тонких решений, связанных с разделением рабочих нагрузок, их распределением по нескольким ядрам процессора и эффективным объединением результатов. Вместо этого разработчик может сосредоточиться на главном: прикладной логике, которая и дает ему заработок.

Parallel.For также предоставляет механизмы более тонкого управления работой цикла. С помощью параметра options, передаваемого в метод For, можно управлять планировщиком, определяющим работу цикла, задавать максимальную степень параллелизма и корректно завершать цикл при запросе отмены:

Такие возможности настройки подчеркивают одну из целей предпринимаемых усилий в расширении поддержки распараллеливания в .NET Framework: существенно облегчить использование параллелизма без усложнения программирования и в то же время предоставить более квалифицированным разработчикам средства, необходимые им для тонкой настройки обработки и выполнения. В этом отношении, кстати, поддерживаются дополнительные средства. Другие перегруженные версии Parallel.For позволяют досрочно выходить из цикла:

Есть также версии, дающие возможность передавать состояние между итерациями, которые выполняются в одном и том же потоке, что обеспечивает гораздо более эффективные реализации таких алгоритмов, как, например, упрощений (reductions):

Класс Parallel поддерживает не только целые диапазоны, но и произвольные источники IEnumerable представление в .NET Framework перечислимой последовательности: код может регулярно вызывать MoveNext в перечислителе, чтобы получать следующее значение Current. Эта возможность использовать произвольные перечислимые последовательности позволяет параллельно обрабатывать произвольные наборы данных независимо от их представления в памяти; более того, источники данных можно материализовать по требованию и загружать, когда вызовы MoveNext достигают еще не материализованных разделов исходных данных:

Parallel.ForEach обеспечивает еще более широкие возможности настройки и контроля, чем Parallel.For. Например, ForEach позволяет определять, как именно будет разделяться входной набор данных. Это делается с помощью набора специальных абстрактных классов, которые позволяют конструкциям распараллеливания запрашивать фиксированное или переменное количество разделов (partitions) и дают им возможность предоставлять абстракции этих разделов над входным набором данных, а также закреплять данные за разделами — статически или динамически:

Parallel.For и Parallel.ForEach дополняются в классе Parallel методом Invoke, принимающим произвольное количество операций, которые нужно запускать с такой степень параллелизма, с какой справится нижележащая система. Эта классическая конструкция fork-join облегчает распараллеливание рекурсивных алгоритмов декомпозиции (divide-and-conquer algorithms), как в часто используемом примере QuickSort:

Несмотря на большой шаг вперед, класс Parallel лишь поверхностно использует доступную функциональность. Одной из более фундаментальных мер, предпринятых в .NET Framework 4 в области распараллеливания, было введение Parallel LINQ, или сокращенно PLINQ (произносится как «Pee-link»). LINQ (Language Integrated Query) появился в .NET Framework версии 3.5. LINQ — это на самом деле две сущности: описание набора операторов, предоставляемых как методы для манипуляций над наборами данных, и совокупность ключевых слов в C# и Visual Basic для выражения запросов непосредственно в языке. Многие из операторов, включенных в LINQ, базируются на эквивалентных давно известных операциях, в том числе Select, SelectMany, Where, Join, GroupBy и еще около 50 других. В .NET Framework Standard Query Operators API определен шаблон для этих методов, но он не описывает, для каких наборов данных предназначены эти операции и как именно следует реализовать эти операции. Далее этот шаблон реализуется различными «провайдерами LINQ» для множества различных источников данных и целевых сред (наборов в памяти, баз данных SQL, объектно-реляционных систем (ORM), вычислительных кластеров HPC Server, временных и потоковых источников данных и т. д.). Один из наиболее часто применяемых провайдеров — LINQ to Objects, и он предоставляет полный набор LINQ-операторов, реализованных поверх IEnumerable . Это обеспечивает реализацию запросов в C# и Visual Basic. Например, следующий фрагмент кода считывает все данные из файла строка за строкой, отфильтровывая строки, содержащие слово «secret» и зашифровывая их; в конечном счете вы получаете перечислимые байтовые массивы:

Для запросов, требующих интенсивных вычислений, или даже запросов, которые вызывают много операций ввода-вывода с длительными задержками, PLINQ предоставляет средства автоматического распараллеливания, реализуя полный набор LINQ-операторов, использующих параллельные алгоритмы. Таким образом, предыдущий запрос можно распараллелить простым добавлением к источнику данных «.AsParallel()»:

Как и в случае класса Parallel, эта модель тоже заставляет разработчика оценивать последствия параллельного выполнения определенных вычислений. Но, как только выбор сделан, система берет на себя обработку низкоуровневых деталей реального распараллеливания, разбиения на разделы, регулирование потоков и прочее. Кроме того, как и Parallel, эти PLINQ-запросы можно настраивать самыми разнообразными способами. Разработчик может управлять тем, как осуществляется секционирование, насколько высокая степень параллелизма применяется на самом деле, контролировать баланс между синхронизацией и задержками и другие вещи:

Илон Маск рекомендует:  Что такое код pcntl_wexitstatus

Эти мощные и высокоуровневые модели программирования для циклов и запросов построены на основе не менее мощного, но низкоуровневого набора API задач, который главным образом опирается на типы Task и Task в пространстве имен System.Threading.Tasks. По сути, механизмы параллельных циклов и запросов являются генераторами задач (task generators), которые опираются на инфраструктуру задач и сопоставляют параллелизм с ресурсами, доступными в нижележащей операционной системе. Task — это фактически представление единицы работы или более обобщенно единицы асинхронности, рабочего элемента, который можно порождать и впоследствии соединять с другими такими элементами различными средствами. Task предоставляет методы Wait, WaitAll и WaitAny, которые обеспечивают синхронное блокирование дальнейшего продвижения до тех пор, пока не будет завершена целевая задача (или задачи) и пока не будут выполнены дополнительные ограничения, переданные перегруженным версиям этих методов (например, истечет время ожидания или появится маркер отмены). Task поддерживает опрос на предмет завершения через свойство IsCompleted и — более обобщенно — опрос на наличие изменений в своем жизненном цикле, что обрабатывается через свойство Status. И, вероятно, самое главное — он предоставляет методы ContinueWith, ContinueWhenAll и ContinueWhenAny, которые позволяют создавать задачи, планируемые, только когда завершен конкретный набор предыдущих задач. Это открывает возможность легко реализовать самые разнообразные сценарии, в том числе выражать зависимости между вычислениями так, чтобы система могла планировать работу на основе выполнения условий этих зависимостей:

Класс Task , производный от Task, позволяет передавать результаты от завершенной операции:

При всех этих моделях (циклах, запросах и задачах) в .NET Framework применяются методики перехвата работы (work-stealing techniques), обеспечивающие более эффективную обработку специализированных рабочих нагрузок; кроме того, по умолчанию используется эвристическая логика поиска экстремума (hill-climbing heuristics) для варьирования количества участвующих потоков в течение времени, чтобы найти оптимальный уровень обработки. Эвристическая логика также встроена в части этих компонентов для автоматического переключения на последовательную обработку, если система считает, что любая попытка распараллеливания приведет к более длительной обработке, чем последовательная; однако эту логику, как и многое другое, что уже обсуждалось, тоже можно изменять.

Task представляет не только операции, связанные с вычислениями. Его можно использовать и для представления произвольных асинхронные операций. Рассмотрим класс System.IO.Stream из .NET Framework, в котором содержится метод Read для извлечения данных из их потока (stream):

Эта операция Read является синхронной и блокирующей, из-за чего поток, вызывающий Read, нельзя использовать для другой работы до завершения этой операции, завязанной на ввод-вывод. Чтобы более высокую масштабируемость, класс Stream предоставляет асинхронный эквивалент метода Read в виде двух методов: BeginRead и EndRead. Эти методы следуют шаблону, существующему в .NET Framework с момента ее появления, а именно шаблону APM (Asynchronous Programming Model). Ниже показана асинхронная версия предыдущего кода:

Однако этот подход ухудшает возможности композиции (composability). Тип TaskCompletionSource устраняет эту проблему, позволяя предоставлять такую асинхронную операцию чтения как задачу:

Благодаря этому становится возможной композиция множества асинхронных операций — так же, как в примерах с чисто вычислительными операциями. В следующем примере осуществляется параллельное чтение изо всех источников — потоков данных и вывод в консоль выполняется, только когда завершены все операции:

Помимо механизмов для распараллеливания и запуска параллельной обработки, .NET Framework 4 также предоставляет примитивы для более глубокой координации работы между задачами и потоками. К ним относится набор масштабируемых и безопасных в условиях многопоточности (thread-safe) типов-наборов, которые в основном исключают необходимость для разработчиков вручную синхронизировать доступ к общим наборам. ConcurrentQueue безопасный в условиях многопоточности, свободный от блокировок FIFO-набор (first-in-first-out), который может одновременно использоваться любым количеством пишущих и читающих потоков. Кроме того, он поддерживает семантику статического множества (snapshot semantics) для параллельных перечислителей, благодаря чему код может анализировать состояние очереди в момент, когда ее изменяют другие потоки. ConcurrentStack аналогичен ConcurrentQueue, но вместо FIFO предоставляет семантику LIFO (last-in-first-out). ConcurrentDictionary словарь, который поддерживает любое количество одновременно обращающихся «читателей», «писателей» и перечислителей. Он также содержит несколько атомарных реализаций многоступенчатых операций, таких как GetOrAdd и AddOrUpdate. Другой тип, ConcurrentBag , предоставляет неупорядоченный набор, использующий очереди с перехватом работы (work-stealing queues).

.NET Framework не останавливается на типах-наборах. Lazy > обеспечивает отложенную инициализацию переменной, используя настраиваемые подходы для достижения безопасности в условиях многопоточности. ThreadLocal предоставляет данные, индивидуальные для потока и экземпляра, инициализацию которых можно откладывать до первого обращения. Тип Barrier обеспечивает поэтапное выполнение, чтобы несколько задач или потоков могли упорядоченно выполнять алгоритм. Список можно продолжить, но все его составляющие исходят из одного главенствующего принципа: разработчики не должны чрезмерно отвлекаться на низкоуровневые и рудиментарные аспекты распараллеливания своих алгоритмов — вместо этого они должны позволить .NET Framework обрабатывать всю механику и детали за них.

Будущее


В будущих версиях .NET Framework поддержка параллелизма и параллельной обработки, заложенная в .NET Framework 4, будет расширена; этого с нетерпением ждут многие разработчики. В фокусе следующих версий .NET Framework будет не только повышение производительности существующих моделей программирования, но и увеличение набора высокоуровневых моделей для охвата большего количества шаблонов параллельных рабочих нагрузок. Одно из таких усовершенствований — новая библиотека для реализации параллельных систем, основанных на потоках данных, и для проектирования приложений с моделями на основе агентов. Новая библиотека System.Threading.Tasks.Dataflow предоставляет множество «блоков потоков данных» («dataflow blocks»), действующих как буферы, процессоры и распространители (propagators) данных. Данные можно асинхронно отправлять в эти блоки, и данные будут обрабатываться и автоматически пересылаться любым связанным приемникам с учетом семантики блока-источника (source block). Библиотека потоков данных также построена поверх задач, причем «за кулисами» эти блоки используют задачи для обработки и распространения данных.

С точки зрения шаблонов, эта библиотека особенна полезна при обработке сетей потоков данных (dataflow networks), образующих цепочки создателей и потребителей данных. Рассмотрим случай, когда данные нужно сжимать, шифровать и записывать в файл, при этом данные поступают из потока (stream) в приложение и проходят через него. Для этого можно было бы сконфигурировать небольшую сеть блоков потока данных:

Помимо библиотеки потоков данных, одним из самых важных новшеств в области параллельного выполнения и обработки в .NET Framework будет первоклассная языковая поддержка в C# и Visual Basic для создания и асинхронного ожидания завершения задач. Эти языки сейчас дополняются средствами перезаписи на основе конечного автомата (state-machine-based rewrite capabilities), которые позволят использовать все последовательные конструкции потоков управления одновременно с асинхронным ожиданием завершения задач. [F# в Visual Studio 2010 поддерживает одну из форм асинхронности как часть своего механизма асинхронных рабочих процессов (asynchronous workflows feature), которая также интегрируется с задачами.] Взгляните на следующий метод, который синхронно копирует данные из одного Stream в другой, возвращая число скопированных байтов:

Реализация этой функции, в том числе ее условий и циклов, с помощью поддержки, например, ранее показанных методов BeginRead/EndRead в Stream, приведет к кошмару из обратных вызовов и логики, которая будет подвержена ошибкам и которую будет очень трудно отлаживать. Вместо этого рассмотрим подход с применением метода ReadAsync, который возвращает Task , и соответствующего метода WriteAsync, возвращающего Task. Используя новую функциональность C#, можно переписать предыдущий метод так:

Обратите внимание на несколько минимальных изменений, потребовавшихся для преобразования синхронного метода в асинхронный. Теперь функция аннотирована как async, чтобы уведомить компилятор о том, что он должен выполнить перезапись этой функции. Благодаря этому всякий раз, когда в Task или Task , запрашивается await-операция, оставшаяся для выполнения часть функции фактически подключается к этой задаче как продолжение (continuation): пока задача не завершится, этот вызов метода не будет занимать какой-либо поток.

Вызов метода Read преобразован в вызов ReadAsync, чтобы ключевое слово await можно было использовать для обозначения точки возврата, где остаток функции должен быть преобразован в продолжение; то же самое относится к WriteAsync. Когда этот асинхронный метод завершается, возвращаемое значение типа long будет приведено к Task ,которое было возвращено изначально вызвавшему метод CopyStreamToStreamAsync, с использованием механизма, подобного уже показанному в случае TaskCompletionSource . Теперь я могу использовать возвращаемое значение от CopyStreamToStreamAsync так же, как и любой Task, ожидать на нем, подключать к нему продолжение и т. д. Благодаря функциональности вроде ContinueWhenAll и WaitAll я могу инициировать множество асинхронных операций и впоследствии соединять их, чтобы добиться более высоких уровней параллельного выполнения и увеличить пропускную способность моего приложения в целом.

Эта языковая поддержка асинхронности делает более эффективными не только операции, связанные с вводом-выводом, но и операции с интенсивным использованием процессора, и, в частности, дает возможность создавать «отзывчивые» клиентские приложения (где нет привязки к UI-потоку и программа не перестает отвечать при любой нагрузке) и при этом по-прежнему использовать преимущества высокой степени распараллеливания. Переход из UI-потока в другой, выполнение любой обработки и возврат результатов в UI-поток для обновления UI-элементов и взаимодействия с пользователем давно уже стали головной болью разработчиков. Языковая поддержка асинхронности взаимодействует с ключевыми компонентами .NET Framework и по умолчанию автоматически возвращает операции в их исходный контекст по завершении await-операции (например, если await-операция инициирована из UI-потока, подключенное продолжение возобновит выполнение в этом потоке). То есть появляется возможность запуска задачи, выполняющей работу, которая требует интенсивного использования вычислительных ресурсов, как фоновой, а разработчик может просто дожидаться получения результатов и сохранять их в UI-элементах, например, так:

Эта фоновая задача может сама порождать множество задач для распараллеливания фоновых вычислений, например с использованием PLINQ-запроса:

Языковую поддержку можно также использовать в сочетании с библиотекой потоков данных, что облегчает естественное выражение сценариев с асинхронными создателями и потребителями данных. Рассмотрим реализацию набора регулируемых создателей, каждый из которых генерирует какие-то данные, отправляемые ряду потребителей. В синхронной реализации можно было бы использовать тип вроде BlockingCollection (see рис. 2), введенный в .NET Framework 4.

Рис. 2. Применение BlockingCollection

Это отличный шаблон — до тех пор, пока в приложении допустимо блокировать потоки как со стороны создателей (генераторов данных), так и их потребителей. Если этот вариант неприемлем, вы можете написать асинхронный эквивалент, используя другой из блоков потоков данных, BufferBlock , и возможность асинхронной передачи данных из блока и их приема, как показано на рис. 3.

Рис. 3. Применение BufferBlock

Здесь методы SendAsync и OutputAvailableAsync возвращают задачи, позволяя компилятору подключать продолжения и обеспечивая асинхронное выполнение всего процесса.

Заключение

PПараллельное программирование давно является уделом разработчиков-экспертов с уникальной квалификацией в искусстве масштабирования кода для использования нескольких ядер процессора. Такими экспертами становятся в результате долгого обучения и накопления многолетнего опыта. Они ценятся очень высоко — их мало. В нашем прекрасном новом мире, где повсеместно распространены многоядерные процессоры, ситуация, когда с задачами распараллеливания могут справляться только эксперты, больше недопустима. Независимо от того, предназначено приложение/компонент для широкой общественности, внутреннего использования или применения в качестве промежуточного инструмента, параллелизм теперь является таким фактором, который нужно как минимум учитывать почти каждому разработчику, и концепцией, которая должна стать доступной широким массам разработчиков, использующим управляемые языки, — пусть даже через компоненты, инкапсулирующие средства распараллеливания. Модели параллельного программирования вроде тех, которые предоставляются в .NET Framework 4 и которые появятся в следующих версиях .NET Framework, сделают реальностью это светлое будущее.

Стефен Тауб ведущий архитектор группы Parallel Computing Platform в Microsoft.

Выражаю благодарность за рецензирование статьи экспертам Джо Хоагу и Дэнни Ши

Параллельное программирование

Дата изменения: 10.10.2020

Начиная с первой версии платформы .NET существовало два основных способа создания многозадачных приложений. Поскольку любое многопоточное программирование основано на принципе асинхронности, каждый из способа применяет собственную модель асинхронности.

Модели асинхронного программирования

  1. Асинхронная модель на основе шаблонов – APM (Asynchronous Programming Model). Ее работа основана на использовании пары методов Begin и End и интерфейсе IAsyncResult.
  2. Асинхронная модель на основе событий – EAP (Event Asynchronous Programming). Она включает в себя: асинхронный метод, который выполняется в отдельном потоке, одно или несколько событий, сигнализирующих об изменении в методе, типов делегатов для передачи обработки событий и обработчиков событий.

Обе модели имеют свои плюсы и минусы, рассмотрение которых требует отдельных разделов. Однако оба они – низкоуровневые и сложные. Поэтому начиная с .NET framework 4.0 способы реализации параллельного программирования пополнились еще одной моделью – TAP (Task-based Asynchronous Pattern), асинхронной моделью на основе задач. В BSL (Base Class Library) добавлена библиотека распараллеливания задач TPL.

В TPL в состав TAP включен примитив асинхронной задачи – класс Task, представляя его как высокоуровневую обертку вокруг асинхронной операции. Благодаря этому классу программист может абстрагироваться от прямой работы с потоками и организовывать параллелизм в программах на основе этого примитива.

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

Использование TAP – это рекомендуемый и современный способ реализации параллелизма в приложениях.

Два типа параллелизма

Существует два типа параллелизма: задач и данных. TPL поддерживает их оба.

Параллелизм на основе задач предполагает, что внутри одно и того же приложения одновременно (параллельно) выполняются несколько взаимозависимых блоков кода (методов). Реализация этого типа в TPL по сути повторяет реализацию в Thread, но на более высоком для программиста уровне. Теперь потоками управляет только пул потоков, который в автоматическом режиме распределяет задачи по свободным потокам выполнения. Количество последних, естественно зависит от ресурсов машины.

Параллелизм данных – это эвфемизм организации совместной работы с одним ресурсом и параллельных вычислений. То есть общая задача разбивается на подзадачи, в каждая подзадача реализуется с помощью отдельного Task’a. Эффективность приложения зависит от сложности целостной задачи и количества подзадач. Чем сложнее задача и больше подзадач – тем выше эффективность. Примером подобной реализации может быть решение многоуровневого уравнения, где приходится вычислять каждую переменную в отдельной задаче, а в общей рассчитывать окончательное решение.

Важно понимать, любые вычисления в TPL выполняются в задаче и только косвенно в потоке. Вся реализация TAP определена внутри пространства имен System.Threading.Task.

Задачи

Класс Task один из основных компонентов параллельного программирования С#, он – оболочка вокруг асинхронной операции, ее абстракция. Поток – это единица выполнения. Абстракции асинхронного метода и единицы выполнения – потока и задачи не взаимно-однозначно. Задачи запускает планировщик задач, а потоки – пул потоков.

Любая задача может запускаться синхронно в том же потоке. Однако, обычно она скрывает в себе асинхронный метод. В контексте библиотеки базовых типов задача – ссылочный нестатический тип. Его объект создается в качестве контейнера для асинхронных операций.

Если асинхронные операции не возвращают значения применяется класс Task, в ином случае – Task .


Механизм работы с задачами в общем случае разделен на 3 этапа. Он не учитывает исключения, или отмену задачи. Их мы будем рассматривать в отдельном разделе.

  1. Создание.
  2. Выполнение.
  3. Ожидание завершения.

Создание и выполнение задачи

Механизм работы с объектами типа Task и Task разделенный, задачу можно создать, но не выполнять, либо создать и сразу выполнить. Первый подход удобен для создания сценариев, когда программист подготавливает задачи для каждого этапа вычислений, а затем запускает их в нужном порядке или запускает только, те, что отвечают некоторым условиям.

Илон Маск рекомендует:  Faq как скрыть таскбар

Объекты классов Task и Task создаются конструкторами этих классов. Самый простой тип конструктора это public Task(Action), где Action – void-делегат, хранящий ссылку на void-метод.

Используем этот конструктор:

Задачи в отличии от потоков не именные, то есть объект класса Task не имеет поля Name и соответствующего свойства, чтобы его прочесть. Единственным способом идентификации задач является считывания поля int ID через статическое свойство CurrentID. Статическим его сделали для того, чтобы использовать внутри асинхронного метода, как в примере выше.

Само поле инициализируется при создании объекта конструктором. Поля уникальны в пределах одного домена приложений.

В листинге выше видно, как задача объявляется и инициализируется конструктором. Создается она запуском метода Start на объекте задачи.

Если необходимо совместить объявление, инициализацию и запуск то рекомендуется использовать статический метод Run(). Его можно использовать, начиная с framework 4.5.

В примере выше создание и выполнение совмещены: объект задачи t инициализируется через лямбда-выражение и сразу же запускается на исполнение параллельно главному потоку.

Чтобы главный поток не успел завершится до выполнения внутренних инструкция асинхронного метода, мы заставили его 5 секунд спать.

Еще более распространенный способ создать и запустить задачу (framework 4.5 и выше) – это использование статического метода StartNew() класса Factory – прямого потомка Task. Это специализированный метод, который позволяет задавать параметры создания задачи и передавать их в планировщик задач.

Следующий код по результатам инструкций эквивалентен коду в предыдущем примере, однако вместо метода Run() используется метод StartNew():

Ожидание завершения задачи

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

Если этого не сделать, то мы можем и не увидеть результатов задачи, если инструкции точке входа (метод Main), следующие за запуском задачи выполняются быстрее чем инструкции самой задачи + время возврата.

Хорошим решением было бы дождаться завершения задачи в методе Main. Для этого в классе Task определен метод Wait(). Он вызывается для объекта уже запущенной задачи. Встречая его, поток управления будет дожидаться завершения этой задачи. Если нужно дождаться завершения более одной задачи применяется методы:

  • WaitAll (объект 1 задачи, объект 2 задачи,…, объект n задачи).
  • WaitAny (tasks Task[]), где Task[] – массив задач.

Чтобы применить ожидание завершения задачи достаточно в одном из листингов выше заменить строку Thread.Sleep() на один из методов ожидания:

Во время ожидания ( в асинхронном методе) может быть сгенерировано одно из исключений : ObjectDisposedException или AggregateException. Первое генерирует сама задача, в которой выполняется асинхронный метод, второе деструктор если ожидаемая задача завершена посредством метода Dispose().

Чаще всего происходит первый случай. Поэтому блок с запуском и ожиданием задачи нередко помещают в блок try/catch для перехвата исключения.

Парадигмы параллельного программирования

В этой теме 0 ответов, 1 участник, последнее обновление Васильев Владимир Сергеевич 2 года/лет, 6 мес. назад.

Статья, написанная студентами для студентов.

Содержание

Обзор предметной области
Парадигма параллельного программирования «клиенты и серверы»
Парадигма параллельного программирования «производители и потребители»
Парадигма параллельного программирования «взаимодействующие равные»
Заключение
Список использованных источников

1 Обзор предметной области

Для того чтобы ознакомиться с видами парадигм параллельного программирования, необходимо начать со знакомства с понятием парадигма программирования.

Парадигма программирования – способ концептуализации, который определяет, как следует проводить вычисления, и как работа, выполняемая компьютером, должна быть структурирована и организована. [1, c. 15].

В данной работе рассматривается парадигма параллельного программирования, то есть работа, выполняемая вычислительной машиной, должна быть организована параллельно.

Парадигмы параллельного программирования в свою очередь подразделяются на следующие основные виды:

  • итеративный параллелизм;
  • рекурсивный параллелизм;
  • «клиенты и серверы»;
  • «производители и потребители»;
  • взаимодействующие равные.

Итеративный параллелизм – процессы выполняют циклические вычисления, решая одну задачу (итерации одного цикла) [2, c. 3]. Чаще всего встречается в вычислениях, выполняемых на нескольких процессорах.

Рекурсивный параллелизм может использоваться, когда в программе есть одна или несколько рекурсивных процедур (функций), и их вызовы независимы, т.е. каждый из них работает над своей частью общих данных. Рекурсивный параллелизм используется для решения таких комбинаторных проблем, как сортировка, планирование (задача коммивояжера) и игры (шахматы и другие) [3, c. 9].

Об остальных перечисленных видах рассказывается в основной части работы.


2 Парадигма параллельного программирования «клиенты и серверы»

Клиенты и серверы – наиболее распространенная модель взаимодействия в распределенных системах, от локальных сетей до всемирной сети Интернет [3, c. 9]. В этой модели реализуется две части: клиент и сервер.

Чаще всего они располагаются на разных машинах, и обычно один сервер обслуживает многих клиентов [4, c. 32]. Клиент отправляет запрос серверу и ждет ответа. Сервер ожидает запр сов от клиентов, а затем действует в соответствии с этими запросами и возвращает ответ клиенту. Иногда клиент может «исполнить» роль сервера, если сам будет получать запросы. Аналогично сервер будет выступать в роли клиента, если ему потребуется обращаться с запросами к другим программам [4].

Например, рассмотрим запрос по World Wide Web, который возникает, когда пользователь открывает новый адрес URL в окне программы-браузера. Web-браузер является клиентским процессом, выполняемым на машине пользователя. Адрес URL косвенно указывает на другую машину, на которой расположена Web-страница. Сама Web-страница доступна для процесса- сервера, выполняемого на другой машине. Этот процесс-сервер читает Web- страницу, определяемую адресом URL, и возвращает ее на машину клиента [3, c. 17].

Отношения программировании между клиентом аналогичны и сервером отношениям в параллельном между программой, вызывающей подпрограмму, и самой подпрограммой в последовательном программировании. Более того, как подпрограмма может быть вызвана из нескольких мест программы, так и у сервера обычно есть много клиентов. Запросы каждого клиента должны обрабатываться независимо, однако параллельно может обрабатываться несколько запросов, подобно тому, как одновременно могут быть активны несколько вызовов одной и той же
процедуры [3, c. 18].

Сервер может быть реализован как одиночный процесс, который не может обрабатывать одновременно несколько клиентских запросов, или (при необходимости параллельного обслуживания запросов) как многопоточная программа.

Клиенты обычно представляют собой множество потоков.
Одна из классических «задач о спящем парикмахере» описывает отношение в системах «клиент-сервер». У парикмахера есть одно рабочее место и приемная с несколькими стульями. Когда парикмахер заканчивает подстригать клиента, он отпускает клиента и затем идет в приёмную, чтобы посмотреть, есть ли там ожидающие клиенты. Если они есть, он приглашает одного из них и стрижет его. Если ждущих клиентов нет, он возвращается к своему креслу и спит в нем.

Каждый приходящий клиент смотрит на то, что делает парикмахер. Если парикмахер спит, то клиент будит его и садится в кресло. Если парикмахер работает, то клиент идет в приёмную. Если в приёмной есть свободный стул, клиент садится и ждёт своей очереди. Если свободного стула нет, то клиент уходит [5].

Клиентами являются, как уже можно догадаться, посетители салона, а парикмахера можно интерпретировать как сервер. Потоки-клиенты отдают запросы серверу, чтобы занять рабочее кресло, сервер получает и обрабатывает поступающие запросы. То есть он возвращает ответ, занят ли ресурс (кресло) или он готов обработать следующий запрос клиента.

Клиенты образуют очередь, которая является ограниченной. Посетители уходят, если нет свободных кресел в зале ожидания. То есть придется создавать и отправлять на выполнение новые потоки (потоки-клиенты), что не является достоинством [6, c. 90].

3 Парадигма параллельного программирования «производители и потребители»

Производители и потребители — это взаимодействующие процессы. Они часто организуются в конвейер, через который проходит информация.

Прежде чем переходить к организации конвейеров, определим, что процесс-производитель генерирует в некотором буфере информацию, которая используется процессом-потребителем.

Здесь возникают проблемы переполнения и исчерпания буфера. Тогда при переполнении буфера производитель должен будет ждать, пока в буфере не освободится хотя бы один элемент, а при исчерпании буфера должен будет ждать потребитель, пока хотя бы один новый элемент не появится в буфере [7].

Как было сказано ранее, часто производители и потребители объединены в конвейер – последовательность процессов, в которой каждый потребляет данные предшественника и поставляет данные для последующего процесса.

Классическим примером являются конвейеры в ОС Unix. Рассмотрим взаимодействие команд:
$ps –Af | grep mc.
Вертикальная черта «|» между командами обозначает конвейер т.е. выход одной команды переназначается на вход другой команды, таким образом, в приведенном выше примере результат выполнения ps –Af будет передан команде grep , которая произведет трансформацию входного потока в соответствии с маской (в данном случае на выходе будут все строки, содержащие подстроку « mc ») [8, c. 22].

Потоки могут быть связаны с файлами особого типа – каналами. Канал – это буфер (очередь типа FIFO) между процессом-производителем ипроцессом-потребителем. Процесс-производитель записывает данные в конец очереди, а процесс-потребитель читает данные из ее начала, при этом символы удаляются. В общем случае канал – это тот самый ограниченный буфер, поэтому производитель при необходимости ожидает, пока в буфере появится свободное место для очередной порции данных, а потребитель при необходимости ждет появления в буфере данных [8, c. 23].

Недостаток данной модели взаимодействия процессов: ограниченность буфера, необходимо всегда проверять размер буфера, иначе возникает опасность переполнения или истощения буфера.

4 Парадигма параллельного программирования взаимодействующие равные

Взаимодействующие равные – модель, в которой исключен не занимающийся непосредственными вычислениями управляющий поток.

Распределение работ в таком приложении либо фиксировано заранее, либо динамически определяется во время выполнения [8, c. 16]. Одним из распространенных способов динамического распределения работ является «портфель задач». Портфель задач, как правило, реализуется с помощью разделяемой переменной, доступ к которой в один момент времени имеет только один процесс [3, c. 26].

Вычислительная задача делится на конечное число подзадач. Как правило, каждая подзадача должна выполнить однотипные действия над разными данными. Подзадачи нумеруются, и каждому номеру определяется функция, которая однозначно отражает номер задачи на соответствующий ему набор данных. Создается переменная, которую следует выполнять следующей. Каждый поток сначала обращается к портфелю задач для выяснения текущего номера задачи, после этого увеличивает его, потом берет соответствующие данные и выполняет задачу, затем обращается к портфелю задач для выяснения следующего номера задачи [8, c. 28].

Естественно должен быть предусмотрен механизм остановки процессов при исчерпывании всего множества задач, как в «производителях и потребителях».

То есть поток получает задачу из портфеля и пока задача остается не выполненной, поток ее решает, а затем снова получает задачу из портфеля.

Рассмотрим задачу вычисления произведения матриц. Подзадачей будет вычисление строк результирующей матрицы, а портфель задач – переменная, в которую будем считать строки. При получении задачи, происходит считывание значения счетчика и его увеличение на 1. Когда счетчик будет равен размерности матрицы, вычисления заканчиваются. То есть поток без прерывания остальных возьмет задачу, при этом выполнив увеличение счетчика.

Таким образом, процессы работают независимо, каждый со своей скоростью, синхронизация происходит с помощью портфеля задач.

Проблема реализации этого алгоритма в том, что доступ к портфелю задач должен быть безопасным. Если между взятием новой задачи и увеличением счетчика работа выполняющего их потока прервется, то некоторую задачу, возможно, отработают несколько потоков. [8, c. 29].

Заключение

В ходе работы были изучены такие парадигмы параллельного программирования, как «клиенты и серверы», «производители и потребители», «взаимодействующие и равные».

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

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

В модели «взаимодействующие равные» проблема схожая с «производителями и потребителями», то есть должен быть механизм приостановки процессов. Также доступ к портфелю задач должен быть безопасным, чтобы не допустить отработку одних и тех же вычислений разными потоками. Но плюсом модели является то, что процессы работают независимо, каждый со своей скоростью.

Параллельное программирование

Читайте также:

  1. B) Параллельное соединение.
  2. II. Параллельное соединение.
  3. VI. Планирование и программирование использования муниципальной собственности
  4. А. Программирование работы гирлянды, работающей в режиме бегущей волны
  5. Алгоритмическое программирование
  6. Введение в визуальное программирование
  7. Введение в математическое программирование.
  8. Введение. Объектно-ориентированное программирование как технология программирования. (4 час.)
  9. Веб-программирование
  10. Вращение без указания осей (плоско-параллельное перемещение)
  11. Встречно-параллельное соединение звеньев
  12. Динамическое программирование

Визуальное программирование

Визуальное программирование основано на ООП и возникло вслед за возникновением и распространением графического интерфейса операционных систем. Основополагающая идея визуального программирования заключается в перетаскивании объектов мышкой из библиотеки (хранилища объектов) в нужное место программы. А система сама должна написать нужный для этого код. Так работают Delphi, C++ Builder, Visual C, Visual Basic и др.

Естественно, каждый объект для этого должен иметь какое-то визуальное представление (иконку), чтобы его можно было перетаскивать мышкой. В результате иногда можно создать нужную программу, вообще не написав ни одной строчки программного кода. Эта технология особенно эффективна при создании интерфейсной части приложений (диалоговых окон, командных кнопок и т.п.).

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

Распространение многоядерных процессоров, на которых стали работать обычные приложения, делает необходимым освоение параллельного программирования со школьной скамьи.

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

В технологии параллельного программирования имеются три дополнительных, чётко определённых этапа.

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

2. Выявление параллелизма: изменение структуры задачи таким образом, чтобы можно было эффективно выполнять подзадачи. Для этого часто требуется найти зависимости между подзадачами и организовать исходный код так, чтобы ими можно было эффективно управлять;

3. Выражение параллелизма: реализация параллельного алгоритма в исходном коде с помощью системы обозначений параллельного программирования.

В настоящее время существует более 100 различных средств, позволяющих разрабатывать параллельные программы. Наиболее известные технологии: OpenMP, стандарт которой был разработан для языков Fortran, C и C++ ; система параллельного программирования PVM (Parallel Virtual Machine), позволяющая объединить набор разных компьютеров, связанных сетью, в общую вычислительную систему, называемую параллельной виртуальной машиной (поддерживает языки Fortran, C, C++, имеет средства сопряжения с языками Perl, Java); технология программирования CUDA (Compute Unified Device Architecture) – программно-аппаратное решение, позволяющее использовать видеопроцессоры NVIDIA для вычислений общего назначения; технология MPI – интерфейс передачи данных (message passing interface).

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

Дата добавления: 2014-01-20 ; Просмотров: 503 ; Нарушение авторских прав? ;

Нам важно ваше мнение! Был ли полезен опубликованный материал? Да | Нет

Понравилась статья? Поделиться с друзьями:
Кодинг, CSS и SQL