Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

u_course

.pdf
Скачиваний:
39
Добавлен:
04.06.2015
Размер:
1.87 Mб
Скачать

Средства разработки параллельных программм

21

потребуется для продолжения выполнения программы. Таковым, например, является Multilisp – параллельная версия известного декларативного языка

Lisp.

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

Параллельное программирование – это та область программирования, которая еще долго будет искусством, а не ремеслом.

ПАРАДИГМЫПАРАЛЛЕЛЬНЫХПРИЛОЖЕНИЙ

Существует ряд приемов написания параллельных программ. Кратко опишем основные.

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

Рекурсивный параллелизм используется в программах с одной или несколькими рекурсивными процедурами, вызов которых независим. Это технология «разделяй-и-властвуй» или «перебор-с-возвратами».

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

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

Средства разработки параллельных программм

22

ент ее вызывает. Однако если код клиента и сервера разнесены в пространстве, то используется удаленный вызов процедур или рандеву.

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

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

Нотация для определения параллельных вычислений

Множество чтения операции – это множество переменных, которые в ходе операции читаются, но не изменяются. Множество записи операции – это множество переменных, которые в ходе операции изменяются (и, возможно, читаются).

Две операции называются независимыми, если их множества записи не пересекаются.

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

В этой и во второй главах для того, чтобы не ограничиваться рамками синтаксиса конкретного языка, мы будем использовать для иллюстрации обсуждаемых алгоритмов общую нотацию, введенную в [17]. Особенности реализации представленных механизмов с помощью библиотеки PTHREAD, функций WinAPI, технологии OpenMP и библиотеки MPI рассматриваются в последующих главах.

Средства разработки параллельных программм

23

Для определения параллельного выполнения нескольких различных потоков команд примем следующую нотацию (оператор co – от

«concurrent»):

co

{ … ;} # группа операторов 1

//# разделитель различных ветвей оператора

#(не путать с комментарием в C!)

{… ;} # группа операторов 2

oc

Обратите внимание на использование знака «#» для комментариев, поскольку традиционный для языка C++ коментарий «//» во вводимой нотации разделяет независимые ветви оператора co. Для независимого выполнения тела цикла для каждого значения счетчика оператор co будем записывать в следующем виде:

co [i = 0 to n-1] {

# параллельное выполнение тела цикла для каждого i

}

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

Другой способ определить параллельные вычисления – использовать декларацию process (оператор co, выполняемый как фоновый):

process p_name [ i=0 to n-1 ] {

# параллельное выполнение каждого из n процессов

}

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

Основное отличие этих двух деклараций в том, что если за декларацией процесса process p_name {…}, существуют операторы, то они выполняются параллельно с новыми процессами. Операторы, следующие за телом оператора co, выполняются только после завершения всех процессов, порожденных в операторе co, т.е. эта нотация предполагает неявную синхронизацию барье-

ром.

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

Средства разработки параллельных программм

24

Итеративный параллелизм: перемножение матриц

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

Задача. Даны матрицы A и B размерностью n × n. Требуется вычислить произведение матриц, поместив результат в матрицу C размерностью n × n.

Пусть n объявлено и инициализировано, пусть также исходные матрицы A, B и матрица результатов C являются разделяемыми, матрицы A и B уже инициализированы.

int n=; # объявление и инициализация n double a[n,n], b[n,n], c[n,n];

… # инициализация матриц А и В

Для вычисления C необходимо вычислить n2 промежуточных произведений. Текст последовательной программы очевиден:

for [ i= 0 to n-1] { for [j = 0 to n-1] {

c[i, j] = 0.0;

for [k = 0 to n-1]

c[i, j] = c[i, j] + a[i, k] * b[k, j];

}

}

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

совым параллелизмом.

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

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

int n=; # объявление и инициализация n double a[n,n], b[n,n], c[n,n];

… # инициализация матриц А и В

Средства разработки параллельных программм

25

co [i = 0 to n-1] { # параллельное вычисление строк for [ j = 0 to n-1] {

c[i, j] = 0.0

for [k = 0 to n-1]

c[i, j] = c[i, j] + a[i, k] * b[k, j];

}

}

Выполнение оператора co порождает столько потоков, сколько строк в матрице A. Распределение потоков по процессорам зависит от среды выполнения и реализации.

Предположим, что число процессоров в системе P < n, n нацело делится на P. Разделим матрицу на полосы строк и столбцов, для каждой полосы создадим свой рабочий процесс, который можно запрограммировать следующим образом.

process worker [wn = 1 to P] { # полосы вычисляются параллельно

int first = (wn -1) * n / P; # номер первой строки полосы для процесса w int last = first + n / P – 1; # номер последней строки в полосе

for [ i= first to last] { for [j = 0 to n-1] {

c[i, j] = 0.0;

for [k = 0 to n-1]

c[i, j] = c[i, j] + a[i, k] * b[k, j];

}

}

}

В этом примере количество потоков (процессов) задано явно.

Рекурсивный параллелизм: адаптивная квадратура

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

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

процедура не обращается к глобальным переменным или только чи-

тает их;

аргументы и результирующие переменные суть различные перемен-

ные.

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

Задача. Дана непрерывная на отрезке [a,b] функция f(x). Требуется приближенно вычислить интеграл

Средства разработки параллельных программм

26

b

f (x)dx

(1)

a

 

 

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

Для реализации первого из них следует разделить интервал [a, b] на фиксированное число отрезков, а затем аппроксимировать интеграл на каждом из отрезков с помощью некоторой квадратурной формулы (например, трапеций или Симпсона). Такая программа будет итеративной, причем она должна вычислять сумму с накоплением. Идея эффективной параллельной организации подобных вычислений рассмотрена при обсуждении парадигмы потребитель-производитель.

Второй способ обычно носит название адаптивной квадратуры и использует метод последовательных приближений с заданной точностью EPS. При его реализации сначала вычисляют аппроксимацию интеграла (1) по двум точкам a и b (начальное приближение). Затем делят отрезок [a, b] на два [a, m] и [m, b], где m в простейшем случае может быть середина начального отрезка. Вычисляют квадратуру как сумму аппроксимаций интеграла на каждом отрезке. Если разность первой и второй квадратур меньше заданной точности EPS, то за приближенное значение интеграла принимают вычисленную сумму. Иначе задача вычисления интеграла (1) делится на две подзадачи вычисления интегралов от a до m и от m до b, процесс повторяется независимо для каждого нового отрезка. Такой алгоритм удачно представить рекурсией.

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

double f (double x);

Определим рекурсивную функцию quad(), реализующую описанный алгоритм. Аргументами quad() являются значения левого left и правого right концов отрезка, значения функций f_left, f_right в этих точках и текущее значение

интеграла lr_integral.

double quad (double left, right, f_left, f_right, lr_integral)

{

double mid = (left + right) / 2; double f_mid =f(mid);

double l_ integral = (f_left + f_mid) * (mid – left) / 2; # аппроксимация по левому отрезку double r_ integral = (f_mid + f_right) * (right - mid) / 2; # аппроксимация

# по правому отрезку

Средства разработки параллельных программм

27

if (abs ((l_ integral +r_ integral)-lr_ integral) > EPS)

{

# рекурсия для интегрирования обоих значений l_ integral = quad(left, mid, f_left, f_mid, l_ integral);

r_ integral = quad(mid, right, f_mid, f_right, r_ integral);

}

return (l_ integral + r_ integral);

}

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

area = quad (a, b, f(a), f(b), (f(a) + f(b))*(b-a) / 2);

Легко заметить, что рекурсивные вызовы функции quad() независимы при условии, что вычисление функции f(x) не дает побочных эффектов. Заменим рекурсивные вызовы в теле функции на оператор co с рекурсивным параллелизмом (разделитель // отделяет процессы, которые допустимо выполнять независимо):

co

l_ integral = quad(left, mid, f_left, f_mid, l_ integral);

//

r_ integral = quad(mid, right, f_mid, f_right, r_ integral);

oc

Оператор co не заканчивается до тех пор, пока не будут завершены оба вызова функций. Таким образом, значения переменных l_ integral и r_ integral вычисляются до того, как функция quad() возвращает их сумму. Проблема может возникнуть в накоплении большого количества параллельно выполняющихся процессов при большой глубине рекурсии, ведь каждый оператор co создает два независимых процесса. Решение этой проблемы, например, может состоять в переключении с параллельных рекурсивных вызовов на последовательные при достижении некоторой критической глубины рекурсии.

Производители и потребители: каналы ОС Unix

Процесс-производитель выполняет вычисления и выводит поток результатов. Процесс-потребитель вводит поток значений и анализирует его. Многие программы в той или иной степени являются производителями и/или потребителями.

Интересен случай, когда производители и потребители объединены в конвейер – последовательность процессов, в которой каждый потребляет данные предшественника и поставляет данные для последующего процесса (рис.1.9). Классическим примером являются конвейеры в ОС Unix. Рассмотрим взаимодействие команд:

$ps –Af | grep mc

Вертикальная черта “|” между командами обозначает конвейер т.е. выход одной команды переназначается на вход другой команды, таким образом, в

Средства разработки параллельных программм

28

приведенном выше примере результат выполнения ps –Af будет передан команде grep которая произведет трансформацию входного потока в соответствии с маской (в данном случае на выходе будут все строки содержащие подстроку «mc»).

Обычно с процессом связан стандартный поток ввода stdin и стандартный поток вывода stdout. Эти потоки могут быть связаны с файлами особого типа – каналами. Канал – это буфер (очередь типа FIFO) между процессомпроизводителем и процессом-потребителем. Он содержит связанную последовательность символов, причем процесс-производитель записывает данные в конец очереди, а процесс-потребитель читает данные из ее начала, при этом символы удаляются. В общем случае канал – это ограниченный буфер, поэтому производитель при необходимости ожидает, пока в буфере появится свободное место для очередной порции данных, а потребитель при необходимости ждет появления в буфере данных.

При общей памяти буферы реализуются с помощью разделяемых переменных и примитивов синхронизации (флагов, семафоров, монитора). При распределенных процессах необходима реализация канала взаимодействия с помощью примитивов пересылки сообщений (send и receive).

Между производителем и потребителем существует однонаправленный поток информации. Такой вид межпроцессного взаимодействия не имеет аналогов в последовательном программировании, поскольку в последовательной программе только один поток управления, в то время как производители и потребители – независимые процессы со своими потоками управления и собственными скоростями выполнения.

Другим способом организации потоков потребителей-производителей являются различные древовидные структуры. Самый распространенный из них – организация процесса вычислений методом сдваивания (или дихотомия). С помощью этого мощного подхода удобно распараллеливать алгоритмы нахождения сумм элементов массива, его максимального элемента и других групповых функций от массивов. Такие задачи часто встречаются, как составляющие алгоритмов (например, описанный выше, первый способ нахождения приближенного значения интеграла). Основную идею метода дихотомии удобно продемонстрировать при распараллеливании алгоритма сложения n чисел. Поскольку в большинстве реальных программ (почему не во всех?) нет существенной разницы, в каком порядке складывать числа, сложение производят по следующей схеме. Сначала находят сумму пар соседних элементов: a(1)+a(2), a(3)+a(4), a(5)+a(6) и т.д. (это можно делать одновременно). На следующем шаге попарно суммируют полученные суммы, и так до получения окончательного результата (рис. 1.8).

Средства разработки параллельных программм

 

 

 

 

29

8 чисел

 

 

 

 

 

 

 

a1

a2

a3

a4

a5

a6

a7

a8

4 процессора

 

 

 

 

 

 

 

a1+ a2

a3+ a4

 

a5+ a6

a7+ a8

2 процессора

a1+ a2+a3+ a4

 

 

a5+ a6+a7+ a8

 

+

 

 

 

 

 

 

 

2 обмена

 

 

 

 

 

 

 

1 процессор

 

a1+ a2+a3+ a4 +a5+ a6+a7+ a8

 

 

+

 

 

 

 

 

 

 

1 обмен

Рис. 1.8. Пример нахождения суммы восьми чисел методом сдваивания

Клиенты и серверы: файловые системы

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

 

Pr1

выход

Pr2

выход

Pr3

выход

Сервер

вход

вход

вход

Клиент1

 

 

 

 

 

 

 

 

 

 

 

 

Клиент2

 

 

 

 

 

 

 

 

 

Рис. 1.9. Конвейер

 

Рис. 1.10. Клиенты и сервер

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

Взаимодействие клиент-сервер чрезвычайно распространено. Это и ОС, и объектно-ориентированные системы, и сети, и СУБД и многое другое.

Средства разработки параллельных программм

30

Задача. Пусть имеется модуль файлового сервера, обеспечивающий две операции работы с файлом: read (читать) и write (писать). Когда процессклиент хочет получить доступ к файлу, он вызывает операцию чтения или записи в соответствующем модуле файлового сервера.

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

Враспределенной системе клиенты и сервер разнесены в пространстве

иобщаются посредством передачи сообщений или вызовом удаленных процедур.

Управляющий и рабочие: распределенное умножение матриц

При обсуждении итеративного параллелизма рассматривался алгоритм параллельного умножения матриц с помощью процессов, которые разделяют общие переменные. На машинах с распределенной памятью любая переменная должна быть локальной для некоторого процесса и может быть доступна только этому процессу. Для обмена информацией процессы используют передачу сообщений. Рассмотрим два способа реализации алгоритма умножения матриц для архитектур с распределенной памятью. В первом случае процессы будут неравноправны – будет выделен управляющий процесс и массив независимых процессов-вычислителей. Во втором случае взаимодействие процессов-вычислителей обеспечивает круговой конвейер (рис. 1.11 – 1.12), хотя это и не исключает наличие управляющего процесса.

Рабочий

Рабочий

...

№ n

№1

№ n

№1

Рабочий

 

Рабочий

Данные

Результат

Данные

 

 

 

 

 

 

Управляющий

 

Управляющий

 

 

 

 

 

Рис. 1.11. Взаимодействие

Рис. 1.12. Круговой конвейер

управляющий-рабочий

 

 

 

На машинах с распределенной памятью любая переменная должна быть локальной для некоторого процесса и может быть доступна только это-

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]