Programming Taskbook


E-mail:

Пароль:

Регистрация пользователя   Восстановление пароля

 

ЮФУ SMBU

Электронный задачник по программированию

©  М. Э. Абрамян (Южный федеральный университет, Университет МГУ-ППИ в Шэньчжэне), 1998–2024

 

PT for MPI | Разработка новых заданий | Примеры

Prev


Примеры разработки заданий по параллельному программированию

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

Создание новой группы учебных заданий

Любая группа заданий должна оформляться в виде динамической библиотеки — dll-файла. Будем разрабатывать библиотеку с новой группой заданий в среде Borland Delphi (при использовании вариантов конструктора для других языков действия по разработке новой группы заданий будут аналогичными). Назовем новую группу MPIDemo (в качестве префикса имени группы, связанной с MPI-программированием, желательно указать текст «MPI», поскольку при наличии такого префикса в имени заданий модуль PT4Load будет создавать для этих заданий специализированные проекты-заготовки, предназначенные для выполнения задания в параллельном режиме). Следуя правилам именования dll-файлов с группами заданий, дополним имя проекта префиксом PT4. Таким образом, имя нашего проекта примет вид PT4MPIDemo.

Начиная с версии 4.13 базового варианта электронного задачника, желательно снабжать имена проектов дополнительным постфиксом, позволяющим определить язык интерфейса для создаваемой группы заданий. Поскольку мы будем описывать разработку дополнительной группы заданий с русским интерфейсом, имя проекта должно быть дополнено постфиксом _ru: PT4MPIDemo_ru. В случае разработки этой же группы заданий на английском языке ее надо будет оформить в виде динамической библиотеки с именем PT4MPIDemo_en.

Создаваемый нами текст библиотеки должен включать ряд стандартных элементов. Перечислим эти элементы:

  • директива uses, обеспечивающая подключение модуля PT4TaskMaker;
  • основная процедура группы заданий, определяющая задание по его номеру (обычно она имеет имя InitTask); данная процедура должна иметь один параметр целого типа и снабжаться модификатором stdcall;
  • процедура инициализации группы заданий, которая должна иметь фиксированное имя inittaskgroup (все буквы строчные) и включаться в список exports процедур, экспортируемых данной библиотекой.

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

По умолчанию группа считается доступной для всех языков программирования, поддерживаемых задачником Programming Taskbook. Поскольку задания по параллельному программированию могут выполняться только для языков Pascal и C++, необходимо добавить в процедуру inittaskgroup фрагмент, позволяющий сделать группу доступной только для указанных языков. Кроме того, целесообразно явным образом установить для создаваемого проекта режимы проверки диапазона {$R+} и поддержки ANSI-строк («длинных строк») {$H+}.

С учетом всех перечисленных требований заготовка нашего проекта примет следующий вид:

{$R+,H+}
library PT4MPIDemo_ru;

uses PT4TaskMaker;

procedure InitTask(num: integer); stdcall;
begin
end;

procedure inittaskgroup;
begin
  if CurrentLanguage and (lgPascal or lgCPP) = 0 then exit;
  CreateGroup('MPIDemo',
    'Примеры задач по параллельному MPI-программированию',
    'М. Э. Абрамян, 2014', 'sddwertfghklbfdgfgd', 1, InitTask);
end;

exports inittaskgroup;

begin
end.

Условный оператор в процедуре inittaskgroup обеспечивает немедленный выход из этой процедуры (без выполнения инициализации группы заданий) в случае, если текущий язык не поддерживается задачником PT for MPI.

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

{$mode delphi}

Во-вторых, в список exports необходимо добавить процедуру activate (описывать эту процедуру не требуется, так как она определена в модуле PT4TaskMaker):

exports inittaskgroup, activate;

Кроме того, главный файл проекта, разрабатываемого в среде Lazarus, должен иметь расширение lpr (а не dpr, как для среды Delphi).

Поскольку наш проект является библиотекой, а не исполняемым приложением, запустить его на выполнение нельзя. Для тестирования динамических библиотек и в среде Delphi, и в среде Lazarus можно использовать главное приложение (host application), которое при запуске подключает библиотеку и тем самым позволяет проверить правильность ее работы. При тестировании библиотеки с учебными заданиями с качестве главного приложения удобно использовать программный модуль PT4Demo.exe, входящий в состав задачника Programming Taskbook и позволяющий отобразить на экране любое задание группы в демонстрационном режиме. С помощью параметров командной строки программу PT4Demo можно настроить на немедленное отображение определенного задания требуемой группы. Не приводя здесь все возможные варианты этих параметров, укажем те, которые являются наиболее удобными для тестирования нашей библиотеки:

-gMPIDemo -n999

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

Для определения главного приложения и его параметров в среде Delphi надо выполнить команду меню «Run | Parameters...», перейти в появившемся окне на вкладку «Locals» и указать полное имя главного приложения в поле «Host Application», а требуемые параметры — в поле «Parameters» (в нашем случае при стандартном размещении задачника Programming Taskbook имя главного приложения будет следующим: C:\Program Files\PT4\PT4Demo.exe). Поле «Working Directory» следует оставить пустым; при этом рабочим каталогом будет считаться тот каталог, который содержит откомпилированную библиотеку с группой заданий. Аналогичное окно настроек предусмотрено и в среде Lazarus; для его отображения надо выполнить команду меню «Run | Run Parameters...».

После выполнения указанных действий мы уже можем протестировать заготовку нашей библиотеки. При нажатии [F9] на экране должно появиться окно задачника с сообщением об ошибке: «Задание MPIDemo1 не реализовано для текущего языка программирования». Это соответствует действительности, так как пока мы не реализовали ни одного задания.

Примечание. Если окно задачника не появилось, то это означает, что в каталоге, в котором разрабатывается библиотека с новой группой заданий, в качестве текущего выбран язык программирования, отличный от Pascal и C++. В этом случае надо выбрать в качестве текущей для данного каталога какую-либо из доступных сред языков Pascal или С++, используя для этого программу PT4Setup, входящую в состав задачника Programming Taskbook. Кроме того, следует убедиться в том, что для задачника установлен русский язык интерфейса. Язык интерфейса, как и текущую среду программирования, можно настроить с помощью программы PT4Setup.

Разработка простого задания: MPIDemo1

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

MPIDemo1. В каждом из процессов, входящих в коммуникатор MPI_COMM_WORLD, прочесть одно вещественное число и вывести его противоположное значение.

Тем не менее, при реализации этого задания придется использовать все новые средства конструктора заданий, связанные с параллельным программированием, — «параллельный» вариант процедуры CreateTask и процедуру SetProcess.

Оформим реализацию задания в виде вспомогательной процедуры MPIDemo1:

procedure MPIDemo1;
var
  i, count, y: integer;
  d: real;
begin
  count := Random(3) + 3;
  CreateTask('Процессы и их ранги', count);
  if count = 0 then exit;
  TaskText('В каждом из процессов, входящих в коммуникатор MPI\_COMM\_WORLD,'#13 +
    'прочесть одно вещественное число и вывести его противоположное значение.');
  if count = 5 then
    y := 1
  else
    y := 2;
  for i := 0 to count - 1 do
  begin
    SetProcess(i);
    d := Random * 199.99 - 100;
    DataR('Процесс ' + Chr(i + 48) + ': ', d, 0, y + i, 6);
    ResultR('Процесс ' + Chr(i + 48) + ': ', -d, 0, y + i, 6);
  end;
end;

Кроме добавления в проект описания процедуры MPIDemo1 необходимо выполнить корректировку процедуры InitTask, включив в нее вызов процедуры MPIDemo1 (в дальнейшем процедура InitTask будет дополняться вызовами процедур, реализующих другие задания нашей группы):

procedure InitTask(num: integer); stdcall;
begin
  case num of
  1: MPIDemo1;
  end;
end;

Теперь при запуске нашего проекта на экране появится окно задачника, подобное приведенному ниже (количество процессов может быть другим). Вначале приводится окно задачника в режиме с динамической компоновкой, а затем — в «старом» режиме с фиксированной компоновкой (для переключения режимов достаточно использовать клавишу [F4]):

Прокомментируем процедуру MPIDemo1.

Первый оператор процедуры определяет с помощью датчика случайных чисел количество процессов, используемое при текущем запуске задания (предварительного вызова процедуры Randomize не требуется, так как она уже была вызвана при выполнении процедуры CreateGroup). Возможное число процессов будет лежать в диапазоне от 3 до 5; такой диапазон выбран для того, чтобы данные для каждого процесса можно было выводить в окне задачника в один столбец, не используя режим прокрутки. Задавать для простых заданий большее число процессов не следует, так как это лишь увеличит время работы параллельной программы. Необходимо только, чтобы при различных запусках программы количество процессов изменялось, пусть и в небольшом диапазоне.

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

Особое внимание надо обратить на следующий условный оператор:

  if count = 0 then exit;

Для того чтобы понять его смысл, необходимо вспомнить механизм выполнения задания в параллельном режиме (см. раздел «Особенности работы задачника в параллельном режиме»). Экземпляр программы, запущенной непосредственно из интегрированной среды, является непараллельной программой-загрузчиком, которая определяет требуемое количество процессов и запускает параллельное приложение. После этого никакие действия, связанные с настройкой задания, ей выполнять не требуется. При выполнении параллельного приложения запускаются несколько экземпляров исходной программы (по одному экземпляру для каждого процесса), однако только в главном процессе (процессе ранга 0) определяются все данные, связанные с заданием; прочие процессы эти данные не формируют, а лишь получают от главного процесса. Таким образом, часть процедуры MPIDemo1, следующую за вызовом процедуры CreateTask, требуется выполнить только для главного процесса параллельного приложения, причем именно возвращаемое значение параметра count позволяет проверить, что данный экземпляр программы выполняется в главном процессе: в этом (и только в этом) случае параметр count отличен от нуля и, кроме того, равен числу запущенных процессов параллельного приложения. Для непараллельной программы- загрузчика и программ, запущенных в качестве подчиненных процессов параллельного приложения, параметр count вернет значение 0, означающее, что дальнейшие действия, определенные в процедуре MPIDemo1, выполнять не требуется. Входное значение параметра count в процессах параллельного приложения не используется; оно учитывается только в программе-загрузчике при запуске программы в параллельном режиме, а также при демо-запуске задания.

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

Следует заметить, что отсутствие оператора if count = 0 then exit; не скажется на предварительном тестировании задания, так как при запуске задания в демо-режиме возвращаемое значение параметра count никогда не будет равно 0. Однако при попытке запуска этого задания в параллельном режиме в случае отсутствия данного оператора будет выведено сообщение об ошибке: «В ситуации, когда параметр ProcessCount процедуры CreateTask вернул значение 0, не выполнен немедленный выход из процедуры инициализации задания».

Рассмотрим оставшиеся операторы процедуры MPIDemo1.

С помощью процедур TaskText задается формулировка задания (обратите внимание на то, что перед символом подчеркивания указывается символ «\»; это связано с тем, что «обычный» символ подчеркивания в формулировках заданий означает переход в режим нижних индексов). Мы использовали новый вариант процедуры TaskText, который появился в конструкторе для версии 4.11 задачника и позволяет указать полный текст формулировки при единственном вызове процедуры TaskText. Необходимо лишь указывать символ #13 между отдельными строками формулировки (при этом начальные и конечные пробелы в каждой строке игнорируются).

Далее задается значение вспомогательной переменной y, которая обеспечивает вертикальное центрирование текста в разделах исходных и результирующих данных: если число процессов равно 3 или 4, то вывод данных начинается со второй строки раздела, а если число равно 5, то с первой.

Наконец, с помощью процедур DataR и ResultR в цикле определяются исходные и результирующие данные. В начале каждой итерации цикла с помощью процедуры SetProcess определяется текущий процесс задания (в качестве параметра процедуры указывается ранг этого процесса). Все последующие данные, определяемые в задании, связываются с текущим процессом. Напомним, что до первого вызова процедуры SetProcess текущим считается процесс ранга 0. В нашем случае с каждым процессом связывается один элемент исходных данных (вещественное число) и один элемент результирующих данных (число, противоположное исходному).

При формировании задания следует также определить количество успешных тестовых испытаний (от 3 до 9), после которых данное задание считается выполненным (успешные тестовые испытания должны быть проведены подряд). Для этого предназначена процедура SetTestCount. Если данная процедура не вызвана (как в нашем случае), то количество тестовых испытаний полагается равным 5, что является достаточным для большинства заданий по параллельному программированию.

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

Для того чтобы задание можно было запустить в параллельном режиме, необходимо установить на компьютере систему MPICH и настроить задачник Programming Taskbook на ее использование (последнее действие выполняется автоматически при запуске программы PT4Setup). Библиотеки с дополнительными группами заданий задачник ищет в рабочем каталоге учащегося и специальном подкаталоге LIB своего системного каталога (обычно системным каталогом задачника является каталог C:\Program Files\PT4). Чтобы не выполнять дополнительных действий по копированию файла PT4MPIDemo_ru.dll в подкаталог LIB, создадим проект-заготовку для выполнения задания MPIDemo1 в том же каталоге, в котором разрабатывается библиотека PT4MPIDemo_ru.dll. Для создания заготовки в требуемом каталоге проще всего воспользоваться ярлыком Load.lnk, размещенным в этом же каталоге. Если данный ярлык в каталоге отсутствует, то это означает, что каталог не является рабочим каталогом задачника; определить его в качестве рабочего каталога можно с помощью программы PT4Setup.

Благодаря префиксу MPI в имени задания, созданная для него заготовка будет учитывать особенности заданий по параллельному MPI-программированию. Так, при выполнении задания MPIDemo1 на языке C++ будет создан файл MPIDemo1.cpp со следующим содержимым:

#include "pt4.h"
#include "mpi.h"

void Solve()
{
  Task("MPIDemo1");
  int flag;
  MPI_Initialized(&flag);
  if (flag == 0)
    return;
  int rank, size;
  MPI_Comm_size(MPI_COMM_WORLD, &size);
  MPI_Comm_rank(MPI_COMM_WORLD, &rank);

}

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

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

Заметим, что при создании данного проекта в рабочий каталог копируются необходимые заголовочные файлы, связанные с библиотекой MPI, а к проекту автоматически подключается файл mpich.lib. Для выполнения простого задания MPIDemo1 достаточно добавить в конец функции Solve следующие три оператора:

double a;
pt >> a;
pt << -a;

Эти операторы обеспечивают чтение одного вещественного числа в каждом процессе и вывод в этом же процессе числа, противоположного прочитанному.

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

Разработка задания, связанного с пересылкой данных: MPIDemo2

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

MPIDemo2. Количество процессов K является четным, в каждом процессе дано целое число. Переслать числа из всех процессов четного ранга (0, 2, …, K – 2) в процесс 0, а числа из всех процессов нечетного ранга (1, 3, …, K – 1) — в процесс 1. В процессах 0 и 1 вывести полученные числа в порядке возрастания рангов переславших их процессов.

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

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

Приведем вид окна задачника для задания MPIDemo2 и одноименную процедуру, реализующую это задание.

procedure MPIDemo2;
var
  i, count, y, k, d: integer;
begin
  count := 2 * (Random(3) + 3);
  CreateTask('Группы процессов и коммуникаторы', count);
  if count = 0 then exit;
  TaskText('Количество процессов~{K} является четным, в каждом процессе дано целое число.'#13 +
    'Переслать числа из всех процессов четного ранга (0,~2,~\., {K}\,\-\,2) в процесс~0,'#13 +
    'а числа из всех процессов нечетного ранга (1,~3,~\., {K}\,\-\,1)~\= в процесс~1.'#13 +
    'В процессах~0 и~1 вывести полученные числа в порядке возрастания рангов'#13 +
    'переславших их процессов.');
  k := count div 2;
  if k = 5 then
    y := 1
  else
    y := 2;
  ResultComment('Процесс 0:', 0, 2);
  ResultComment('Процесс 1:', 0, 4);
  for i := 0 to k - 1 do
  begin
    d := 10 + Random(90);
    SetProcess(2 * i);
    DataN('Процесс ' + Chr(2 * i + 48) + ': ', d, xLeft,
      y + i, 2);
    SetProcess(0);
    ResultN('', d, Center(i + 1, k, 2, 2), 3, 2);
    d := 10 + Random(90);
    SetProcess(2 * i + 1);
    DataN('Процесс ' + Chr(2 * i + 49) + ': ', d, xRight,
      y + i, 2);
    SetProcess(1);
    ResultN('', d, Center(i + 1, k, 2, 2), 5, 2);
  end;
end;

Прокомментируем приведенную процедуру.

Задание отнесено к подгруппе «Группы процессов и коммуникаторы» (это имя указано в качестве первого параметра процедуры CreateTask), так как наиболее эффективное его решение связано с применением вспомогательных коммуникаторов.

В тексте формулировки задания используется ряд управляющих последовательностей (команд): фигурные скобки выделяют имена переменных, символ «~» обозначает стандартный неразрывный пробел, команда «\,» — малый неразрывный пробел, команда «\=» обозначает тире (—), команда «\-» — знак «минус» (–), команда «\.» — многоточие (…). Некоторые из этих последовательностей учитываются только при выводе формулировки задания в html-документе, который можно сгенерировать, например, с помощью модуля PT4Demo (см. образец html-документа в конце данного раздела). В частности, в html-документе все переменные выделяются курсивом.

Для размещения элемента данных вместе с его комментарием по центру левого или правого столбца в качестве параметра X используются константы xLeft и xRight соответственно. Для центрирования элемента по горизонтали относительно всего раздела можно использовать константу xCenter, но проще явно указать ее значение, равное 0 (мы использовали это значение в процедурах ResultComment). В процедуре ResultN используется вспомогательная функция Center(I, N, W, B), обеспечивающая центрирование по горизонтали набора элементов: она возвращает позицию, с которой надо вывести I-й элемент набора из N элементов (I меняется от 1 до N) при условии, что ширина каждого элемента равна W позициям, а между соседними элементами надо указывать B пробелов.

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

После добавления в библиотеку PT4MPIDemo процедуры MPIDemo2 необходимо включить ее вызов в процедуру InitTask:

procedure InitTask(num: integer); stdcall;
begin
  case num of
  1: MPIDemo1;
  2: MPIDemo2;
  end;
end;

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

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

MPI_Comm c;
MPI_Comm_split(MPI_COMM_WORLD, rank % 2, rank, &c);
int n, res[5];
pt >> n;
MPI_Gather(&n, 1, MPI_INT, res, 1, MPI_INT, 0, c);
if (rank < 2)
  for (int i = 0; i < size / 2; ++i)
    pt << res[i];

Вначале с помощью функции MPI_Comm_split исходный коммуникатор расщепляется на два, каждый из которых связывается с процессами одинаковой четности. Затем с помощью функции MPI_Gather данные из всех процессов, входящих в новые коммуникаторы, пересылаются в начальный процесс каждого коммуникатора. В исходном коммуникаторе MPI_COMM_WORLD начальные процессы двух созданных коммуникаторов имеют ранг 0 и 1, поэтому для определения процессов, в которых надо вывести полученные данные, достаточно использовать условие rank < 2.

Разработка сложного задания: MPIDemo3

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

MPIDemo3. В процессе 0 дано число N и вектор b размера N с вещественными элементами. В остальных процессах даны строки вещественной квадратной матрицы A порядка N, причем в каждом процессе вначале указывается число строк K, затем для каждой строки указывается ее порядковый номер в матрице и элементы этой строки. Найти произведение A·b и вывести его элементы в процессе 0.

Главной особенностью этого задания по сравнению с ранее рассмотренными является то, что каждый процесс содержит большое число исходных данных, и поэтому разместить на одной экранной строке информацию о нескольких процессах не удастся. Более того, для повышения наглядности следует разбить данные для каждого процесса на несколько экранных строк, отведя одну строку для вывода комментария «Процесс <номер процесса>» и значения N (для главного процесса) или K (для подчиненных процессов), а последующие строки — для вывода данных, связанных с вектором b (для главного процесса) или с элементами соответствующих строк матрицы (для подчиненных процессов). Очевидно, что пяти экранных строк для отображения всех перечисленных данных будет недостаточно, поэтому воспользуемся в разделе исходных данных режимом прокрутки.

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

procedure Swap(var a, b: integer);
var x: integer;
begin
  x := a;
  a := b;
  b := x;
end;

procedure MPIDemo3;
var
  i, j, count, n, y, cnt, m: integer;
  a: array[1..10, 1..10] of real;
  b, c: array[1..10] of real;
  k, num: array[1..10] of integer;
begin
  count := Random(4) + 3;
  CreateTask('Параллельная обработка матриц', count);
  if count = 0 then exit;
  TaskText(
    'В процессе~0 дано число~{N} и вектор~{b} размера~{N} с вещественными элементами.'#13 +
    'В остальных процессах даны строки вещественной квадратной матрицы~{A} порядка~{N},'#13 +
    'причем в каждом процессе вначале указывается число строк~{K}, затем для каждой'#13 +
    'строки указывается ее порядковый номер в матрице и элементы этой строки.'#13 +
    'Найти произведение~{A}\*{b} и вывести его элементы в процессе~0.');
  // Формирование матрицы a и вектора b; n - порядок матрицы
  n := Random(4) + 6;
  for i := 1 to n do
  begin
    b[i] := Random * 9.95;
    for j := 1 to n do
      a[i, j] := Random * 9.95;
    num[i] := i;
  end;
  // Вычисление произведения Ab в массиве c
  for i := 1 to n do
  begin
    c[i] := 0;
    for j := 1 to n do
      c[i] := c[i] + a[i, j] * b[j];
  end;
  // Получение в массиве num случайного порядка
  // номеров строк матрицы
  for i := 1 to n do
    Swap(num[i], num[Random(n) + 1]);
  // Определение числа строк матрицы, передаваемых каждому
  // процессу (в процесс i передаются k[i] строк матрицы)
  repeat
    k[count - 1] := n;
    for i := 1 to count - 2 do
    begin
      k[i] := Random(3) + 1;
      k[count - 1] := k[count - 1] - k[i];
    end;
  until k[count - 1] > 0;
  // Вывод исходных и результирующих данных для процесса 0
  SetProcess(0);
  DataN('Процесс 0: N = ', n, 4, 1, 1);
  for i := 1 to n do
    DataR('', b[i], Center(i, n, 4, 2), 2, 4);
  ResultComment('Процесс 0:', 0, 2);
  for i := 1 to n do
    ResultR('', c[i], Center(i, n, 6, 2), 3, 6);
  // Вывод исходных данных для остальных процессов
  y := 2;
  cnt := 0;
  for i := 1 to count - 1 do
  begin
    SetProcess(i);
    y := y + 1;
    DataN('Процесс ' + Chr(i + 48) + ': K = ', k[i], 4, y, 1);
    for m := 1 to k[i] do
    begin
      y := y + 1;
      cnt := cnt + 1;
      DataN('', num[cnt], 8, y, 1);
      for j := 1 to n do
        DataR('', a[num[cnt], j], Center(j, n, 4, 2), y, 4);
    end;
  end;
end;

Для определения текущего номера экранной строки в разделе исходных данных используется переменная-счетчик y. Как только при формировании исходных данных будет использована строка с номером y, большим 5, в разделе исходных данных будет включен режим прокрутки. Напомним, что режим прокрутки для раздела исходных данных и раздела результатов является новой возможностью конструктора PT4TaskMaker, специально введенной для использования в заданиях по параллельному программированию (хотя, разумеется, прокрутку можно использовать и при разработке обычных, «непараллельных» заданий). Обратите внимание на то, что при использовании режима с динамической компоновкой разделов (см. второй из приведенных выше рисунков) раздел с исходными данными полностью отображается в окне задачника и, таким образом, необходимости в дополнительной прокрутке данных не возникает.

После добавления в файл PT4MPIDemo процедуры MPIDemo3 необходимо изменить на 3 значение предпоследнего параметра процедуры CreateGroup и откорректировать процедуру InitTask:

procedure InitTask(num: integer); stdcall;
begin
  case num of
  1: MPIDemo1;
  2: MPIDemo2;
  3: MPIDemo3;
  end;
end;

Приведем решение данного задания на языке C++, не указывая ту часть функции Solve, которая создается при генерации проекта-заготовки:

int n;
double b[10], c;
if (rank == 0)
{
  pt >> n;
  for (int i = 0; i < n; ++i)
    pt >> b[i];
}
MPI_Bcast(&n, 1, MPI_INT, 0, MPI_COMM_WORLD);
MPI_Bcast(b, n, MPI_DOUBLE, 0, MPI_COMM_WORLD);
if (rank != 0)
{
  int k;
  pt >> k;
  for (int k0 = 0; k0 < k; ++k0)
  {
    int m;
    pt >> m;
    c = 0;
    for (int i = 0; i < n; ++i)
    {
      double a;
      pt >> a;
      c += b[i]*a;
    }
    MPI_Send(&c, 1, MPI_DOUBLE, 0, m - 1, MPI_COMM_WORLD);
  }
}
else
{
  for (int i = 0; i < n; ++i)
  {
    MPI_Status s;
    MPI_Recv(&c, 1, MPI_DOUBLE, MPI_ANY_SOURCE, MPI_ANY_TAG,
      MPI_COMM_WORLD, &s);
    b[s.MPI_TAG] = c;
  }
  for (int i = 0; i < n; ++i)
    pt << b[i];
}

В этом решении используется как групповая пересылка данных (из процесса 0 в подчиненные процессы с применением функции MPI_Bcast), так и пересылка данных между двумя процессами (реализуемая функциями MPI_Send и MPI_Recv).

Следует обратить внимание на две особенности использования функций MPI_Send и MPI_Recv: во-первых, при пересылке найденного элемента результирующего вектора связанная с этим элементом информация (порядковый номер элемента) пересылается в виде параметра msgtag (и поэтому в принимающей функции MPI_Recv в качестве этого параметра указана константа MPI_ANY_TAG); во-вторых, принимающий процесс ранга 0 «не знает», сколько сообщений будет ему послано каждым из подчиненных процессов; ему известно лишь общее число посланных ему сообщений (равное размеру результирующего вектора), поэтому в принимающей функции он использует константу MPI_ANY_SOURCE в качестве «заменителя» ранга процесса-получателя.

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

Импортирование задания, добавление описания группы и просмотр заданий в формате html

В конструкторе учебных заданий предусмотрена возможность импортирования существующих заданий в новую группу. Для того чтобы познакомиться с этой возможностью, импортируем в группу MPIDemo задание MPIBegin83 — первое из заданий группы MPIBegin, посвященное виртуальным топологиям.

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

procedure InitTask(num: integer); stdcall;
begin
  case num of
  1: MPIDemo1;
  2: MPIDemo2;
  3: MPIDemo3;
  4: UseTask('MPIBegin', 83);
  end;
end;

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

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

Запустив наш проект на выполнение, мы увидим в окне задачника описание импортированного задания:

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

Возможность импортирования заданий полезна, если необходимо перегруппировать имеющиеся задания, исключив некоторые из них (слишком простые или слишком сложные) и/или изменив порядок их следования. Группы, целиком состоящие из импортированных заданий, называются сводными группами. Для создания таких групп в задачнике предусмотрен специальный механизм, не требующий разработки проектов в интегрированных средах (таких, как Borland Delphi или Free Pascal Lazarus). Достаточно подготовить текстовый файл, указав в нем в определенном формате необходимые данные о новой сводной группе (название и описание группы, имена включаемых в нее заданий), и обработать его с помощью программы PTVarMaker «Конструктор вариантов» (данная программа, как и конструктор учебных заданий, входит в комплекс Teacher Pack for PT4). В результате на основе указанных данных будет автоматически сгенерирована динамическая библиотека с реализацией сводной группы (подробное описание механизма быстрого создания сводных групп содержится в соответствующем разделе описания системы Teacher Pack).

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

Комментарии можно добавлять не только к группе, но и к ее отдельным подгруппам. Кроме того, комментарии, как и задания, можно импортировать из других групп.

Продемонстрируем различные способы добавления комментариев. Добавим к создаваемой нами группе преамбулу, часть которой будет импортирована из группы MPIBegin. Кроме того, добавим преамбулу к подгруппе «Виртуальные топологии», в которой укажем, что содержащееся в ней задание MPIDemo4 импортировано из группы MPIBegin.

Все действия по добавлению комментариев следует выполнять в процедуре инициализации группы inittaskgroup. Добавим в конец этой процедуры (после вызова процедуры CreateGroup) следующие операторы:

CommentText('Данная группа демонстрирует различные возможности');
CommentText('конструктора \MPT4TaskMaker\m при разработке');
CommentText('заданий по параллельному программированию.\P');
UseComment('MPIBegin');
Subgroup('Виртуальные топологии');
CommentText('Данное задание дублирует задание MPIBegin83.');

Три первых вызова процедуры CommentText определяют начальную часть преамбулы для группы MPIDemo. Обратите внимание на управляющие последовательности: парные последовательности \M и \m выделяют фрагмент, в котором используется моноширинный шрифт, а последовательность \P обеспечивает переход к новому абзацу.

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

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

Для того чтобы ознакомиться с результатом сделанных изменений, следует сгенерировать html-страницу с текстом группы MPIDemo. Для этого достаточно внести небольшое изменение в параметры командной строки главного приложения PT4Demo.exe, дополнив параметр –g символом # (в результате список параметров примет вид -gMPIDemo# -n999). Теперь при запуске проекта PT4MPIDemo на экране вместо окна задачника с заданием MPIDemo4 будет отображаться html-браузер с описанием созданной группы (обратите внимание на два последних абзаца в преамбуле группы, которые был импортированы из группы MPIBegin):

Примечание. Для генерации html-описания группы заданий с помощью модуля PT4Demo не обязательно использовать параметры командной строки. Достаточно запустить модуль PT4Demo с помощью ярлыка Demo.lnk, содержащегося в рабочем каталоге, выбрать из списка групп нужную группу и нажать клавишу [F2] или кнопку в окне данного модуля. Вывести html-описание группы можно также, используя программу-заготовку, созданную для выполнения задания. Для этого достаточно изменить параметр в процедуре Task, удалив в нем номер и добавив символ #, например, Task('MPIDemo#'). Заметим, что если указать в параметре символ #, не удаляя номер задания (например, Task('MPIDemo4#')), то в html-описание будет включено только задание с указанным номером. При этом будут также выведены комментарии ко всей группе и к той подгруппе, к которой относится выбранное задание.


Prev

 

Рейтинг@Mail.ru

Разработка сайта:
М. Э. Абрамян, В. Н. Брагилевский

Последнее обновление:
01.01.2024