Обрамляющие функции. Начало и завершение

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

 

Инициализация библиотеки. Одна из первых инструкций в функции main (главной функции приложения):

 

MPI_Init( &argc, &argv );

 

Она получает адреса аргументов, стандартно получаемых самой main от операционной системы и хранящих параметры командной строки. В конец командной строки программы MPI-загрузчик mpirun добавляет ряд информационных параметров, которые требуются MPI_Init.

 

Аварийное закрытие библиотеки. Вызывается, если пользовательская программа завершается по причине ошибок времени выполнения, связанных с MPI:

 

MPI_Abort( описатель области связи, код ошибки MPI );

 

Вызов MPI_Abort из любой задачи принудительно завершает работу ВСЕХ задач, подсоединенных к заданной области связи. Если указан описатель MPI_COMM_WORLD, будет завершено все приложение (все его задачи) целиком, что, по-видимому, и является наиболее правильным решением. Используйте код ошибки MPI_ERR_OTHER, если не знаете, как охарактеризовать ошибку в классификации MPI.

 

Нормальное закрытие библиотеки:

 

MPI_Finalize();

 

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

ü перед вызовом стандартной функции Си exit;

ü перед каждым после MPI_Init оператором return в функции main;

ü если функции main назначен тип void, и она не заканчивается оператором return, то MPI_Finalize() следует поставить в конец main.

 

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

 

int size, rank;

MPI_Comm_size( MPI_COMM_WORLD, &size );

MPI_Comm_rank( MPI_COMM_WORLD, &rank );

 

Пример применения этих функций находится в файле “pi_mpi.c” – нахождение числа Пи. В нем используются, кроме уже знакомых функций, MPI_Bcast и MPI_Reduce. Эти функции рассматриваются в параграфах: MPI_Bcast – Функции коллективного обмена данными; MPI_Reduce – Функции поддержки распределенных операций).

2.4. Связь "точка-точка"

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

 

Задача 1 передает:

 

int buf[10];

MPI_Send( buf, 5, MPI_INT, 1, 0, MPI_COMM_WORLD );

 

Задача 2 принимает:

 

int buf[10];

MPI_Status status;

MPI_Recv( buf, 10, MPI_INT, 0, 0, MPI_COMM_WORLD, &status );

 

Аргументы функций:

 

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

 

2.Размер буфера. Задается не в байтах, а в количестве ячеек. Для MPI_Send указывает, сколько ячеек требуется передать (в примере передаются 5 чисел). В MPI_Recv означает максимальную емкость приемного буфера. Если фактическая длина пришедшего сообщения меньше - последние ячейки буфера останутся нетронутыми, если больше - произойдет ошибка времени выполнения.

 

3.Тип ячейки буфера. MPI_Send и MPI_Recv оперируют массивами однотипных данных. Для описания базовых типов Си в MPI определены константы MPI_INT, MPI_CHAR, MPI_DOUBLE и так далее, имеющие тип MPI_Datatype. Их названия образуются префиксом "MPI_" и именем соответствующего типа (int, char, double, ...), записанным заглавными буквами. Пользователь может "регистрировать" в MPI свои собственные типы данных, например, структуры, после чего MPI сможет обрабатывать их наравне с базовыми.

 

4.Номер задачи, с которой происходит обмен данными. Все задачи внутри созданной MPI группы автоматически нумеруются от 0 до (размер группы-1). В примере задача 0 передает задаче 1, задача 1 принимает от задачи 0.

 

5.Идентификатор сообщения. Это целое число от 0 до 32767, которое пользователь выбирает сам. Оно служит той же цели, что и, например, расширение файла - задача-приемник:

ü по идентификатору определяет смысл принятой информации;

ü сообщения, пришедшие в неизвестном порядке, может извлекать из общего входного потока в нужном алгоритму порядке. Хорошим тоном является обозначение идентификаторов символьными именами посредством операторов "#define" или "const int".

 

6.Описатель области связи (коммуникатор). Должен быть одинаковым для MPI_Send и MPI_Recv.

 

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

2.5. Прием и передача: MPI_Sendrecv

Некоторые конструкции с приемо-передачей применяются очень часто. Пример – обмен данными с соседями по группе (для четного количества ветвей в группе):

 

MPI_Comm_size( MPI_COMM_WORLD, &size );

MPI_Comm_rank( MPI_COMM_WORLD, &rank );

if( rank % 2 ) {

/* Ветви с четными номерами сначала

* передают следующим нечетным ветвям,

* потом принимают от предыдущих

*/

MPI_Send(..., ( rank+1 ) % size ,...);

MPI_Recv(..., ( rank+size-1 ) % size ,...);

} else {

/* Нечетные ветви поступают наоборот:

* сначала принимают от предыдущих ветвей,

* потом передают следующим.

*/

MPI_Recv(..., ( rank-1 ) % size ,...);

MPI_Send(..., ( rank+1 ) % size ,...);

}

 

Другой пример – посылка данных и получение подтверждения:

 

MPI_Send(..., anyRank ,...); /* Посылаем данные */

MPI_Recv(..., anyRank ,...); /* Принимаем подтверждение */

 

Ситуация настолько распространенная, что в MPI специально введены две функции, осуществляющие одновременно посылку одних данных и прием других. Первая из них - MPI_Sendrecv. Она имеет 12 параметров: первые 5 параметров такие же, как у MPI_Send, остальные 7 параметров такие же, как у MPI_Recv. Один ее вызов проделывает те же действия, для которых в первом фрагменте требуется блок IF-ELSE с четырьмя вызовами. Следует учесть, что:

ü и прием, и передача используют один и тот же коммуникатор;

ü порядок приема и передачи данных MPI_Sendrecv выбирает автоматически; гарантируется, что автоматический выбор не приведет к "клинчу";

ü MPI_Sendrecv совместима с MPI_Send и MPI_Recv, т.е может "общаться" с ними.

 

MPI_Sendrecv_replace помимо общего коммуникатора использует еще и общий для приема-передачи буфер. Не очень удобно, что параметр count получает двойное толкование: это и количество отправляемых данных, и предельная емкость входного буфера. Показания к применению:

ü принимаемые данные должны быть заведомо НЕ ДЛИННЕЕ отправляемых;

ü принимаемые и отправляемые данные должны иметь одинаковый тип;

ü отправляемые данные затираются принимаемыми.

 

MPI_Sendrecv_replace так же гарантированно не вызывает клинча.

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

 

-- Ветвь 1 -- -- Ветвь 2 --

Recv( из ветви 2 ) Recv( из ветви 1 )

Send( в ветвь 2 ) Send( в ветвь 1 )

 

Это вызовет клинч: функция приема не вернет управления до тех пор, пока не получит данные; поэтому функция передачи не может приступить к отправке данных; поэтому функция приема... и так до самого SIG_KILL.

Коллективные функции

Под термином "коллективные" в MPI подразумеваются три группы функций:

ü функции коллективного обмена данными;

ü точки синхронизации, или барьеры;

ü функции поддержки распределенных операций.

 

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

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